diff --git a/.circleci/config.yml b/.circleci/config.yml index 92a2b1d9e50f..cbb673207317 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -122,6 +122,7 @@ jobs: - mix.exs - mix.lock - appspec.yml + - rel check_formatted: docker: # Ensure .tool-versions matches @@ -279,6 +280,32 @@ jobs: name: Jest command: ./node_modules/.bin/jest working_directory: apps/block_scout_web/assets + release: + docker: + # Ensure .tool-versions matches + - image: circleci/elixir:1.7.2 + environment: + MIX_ENV: prod + + working_directory: ~/app + + steps: + - attach_workspace: + at: . + + - run: mix local.hex --force + - run: mix local.rebar --force + - run: mix release --verbose --env prod + - run: + name: Collecting artifacts + command: | + find -name 'blockscout.tar.gz' -exec sh -c 'mkdir -p ci_artifact && cp "$@" ci_artifact/ci_artifact_blockscout.tar.gz' _ {} + + when: always + + - store_artifacts: + name: Uploading CI artifacts + path: ci_artifact/ci_artifact_blockscout.tar.gz + destination: ci_artifact_blockscout.tar.gz sobelow: docker: # Ensure .tool-versions matches @@ -561,6 +588,9 @@ workflows: - jest: requires: - build + - release: + requires: + - build - sobelow: requires: - build diff --git a/.credo.exs b/.credo.exs index ceb9421811a2..f7c4edb32a83 100644 --- a/.credo.exs +++ b/.credo.exs @@ -75,8 +75,9 @@ # Priority values are: `low, normal, high, higher` # {Credo.Check.Design.AliasUsage, - excluded_namespaces: ~w(Block Blocks Import Socket Task), - excluded_lastnames: ~w(Address DateTime Exporter Fetcher Full Instrumenter Monitor Name Number Repo Time Unit), + excluded_namespaces: ~w(Block Blocks Import Socket SpandexDatadog Task), + excluded_lastnames: + ~w(Address DateTime Exporter Fetcher Full Instrumenter Logger Monitor Name Number Repo Spec Time Unit), priority: :low}, # For some checks, you can also set other parameters diff --git a/README.md b/README.md index f201753977b1..9b08f7c246fc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

BlockScout

Blockchain Explorer for inspecting and analyzing EVM Chains.

- + [![CircleCI](https://circleci.com/gh/poanetwork/blockscout.svg?style=svg&circle-token=f8823a3d0090407c11f87028c73015a331dbf604)](https://circleci.com/gh/poanetwork/blockscout) [![Coverage Status](https://coveralls.io/repos/github/poanetwork/blockscout/badge.svg?branch=master)](https://coveralls.io/github/poanetwork/blockscout?branch=master) [![Join the chat at https://gitter.im/poanetwork/blockscout](https://badges.gitter.im/poanetwork/blockscout.svg)](https://gitter.im/poanetwork/blockscout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -245,6 +245,22 @@ To view Modules and API Reference documentation: 2. View the generated docs. `open doc/index.html` +## Front-end + +### Javascript + +All Javascript files are under [apps/block_scout_web/assets/js](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js) and the main file is [app.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/app.js). This file imports all javascript used in the application. If you want to create a new JS file consider creating into [/js/pages](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js/pages) or [/js/lib](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js/lib), as follows: + +#### js/lib +This folder contains all scripts that can be reused in any page or can be used as a helper to some component. + +#### js/pages +This folder contains the scripts that are specific for some page. + +#### Redux +This project uses Redux to control the state in some pages. There are pages that have things happening in real-time thanks to the Phoenix channels, e.g. Address page, so the page state changes a lot depending on which events it is listening. The redux is also used to load some contents asynchronous, see [async_listing_load.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/lib/async_listing_load.js). + +To understand how to build new pages that need redux in this project, see the [redux_helpers.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/lib/redux_helpers.js) ## Internationalization @@ -283,6 +299,33 @@ BlockScout is setup to export [Prometheus](https://prometheus.io/) metrics at `/ 3. Click "Load" 6. View the dashboards. (You will need to click-around and use BlockScout for the web-related metrics to show up.) +## Tracing + +Blockscout supports tracing via +[Spandex](http://git@github.com:spandex-project/spandex.git). Each application +has its own tracer, that is configured internally to that application. In order +to enable it, visit each application's `config/.ex` and update its tracer +configuration to change `disabled?: true` to `disabled?: false`. Do this for +each application you'd like included in your trace data. + +Currently, only [Datadog](https://www.datadoghq.com/) is supported as a +tracing backend, but more will be added soon. + +### DataDog + +If you would like to use DataDog, after enabling `Spandex`, set +`"DATADOG_HOST"` and `"DATADOG_PORT"` environment variables to the +host/port that your Datadog agent is running on. For more information on +Datadog and the Datadog agent, see their +[documentation](https://docs.datadoghq.com/). + +### Other + +If you want to use a different backend, remove the +`SpandexDatadog.ApiServer` `Supervisor.child_spec` from +`Explorer.Application` and follow any instructions provided in `Spandex` +for setting up that backend. + ## Memory Usage The work queues for building the index of all blocks, balances (coin and token), and internal transactions can grow quite large. By default, the soft-limit is 1 GiB, which can be changed in `apps/indexer/config/config.exs`: diff --git a/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js b/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js new file mode 100644 index 000000000000..6e86d8a6b5a2 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js @@ -0,0 +1,80 @@ +import { asyncReducer, asyncInitialState } from '../../js/lib/async_listing_load' + +describe('ELEMENTS_LOAD', () => { + test('sets only nextPagePath and ignores other keys', () => { + const state = Object.assign({}, asyncInitialState) + const action = { type: 'ELEMENTS_LOAD', nextPagePath: 'set', foo: 1 } + const output = asyncReducer(state, action) + + expect(output.foo).not.toEqual(1) + expect(output.nextPagePath).toEqual('set') + }) +}) + +describe('ADD_ITEM_KEY', () => { + test('sets itemKey to what was passed in the action', () => { + const expectedItemKey = 'expected.Key' + + const state = Object.assign({}, asyncInitialState) + const action = { type: 'ADD_ITEM_KEY', itemKey: expectedItemKey } + const output = asyncReducer(state, action) + + expect(output.itemKey).toEqual(expectedItemKey) + }) +}) + +describe('START_REQUEST', () => { + test('sets loading status to true', () => { + const state = Object.assign({}, asyncInitialState, { loading: false }) + const action = { type: 'START_REQUEST' } + const output = asyncReducer(state, action) + + expect(output.loading).toEqual(true) + }) +}) + +describe('REQUEST_ERROR', () => { + test('sets requestError to true', () => { + const state = Object.assign({}, asyncInitialState, { requestError: false }) + const action = { type: 'REQUEST_ERROR' } + const output = asyncReducer(state, action) + + expect(output.requestError).toEqual(true) + }) +}) + +describe('FINISH_REQUEST', () => { + test('sets loading status to false', () => { + const state = Object.assign({}, asyncInitialState, { + loading: true, + loadingFirstPage: true + }) + const action = { type: 'FINISH_REQUEST' } + const output = asyncReducer(state, action) + + expect(output.loading).toEqual(false) + expect(output.loadingFirstPage).toEqual(false) + }) +}) + +describe('ITEMS_FETCHED', () => { + test('sets the items to what was passed in the action', () => { + const expectedItems = [1, 2, 3] + + const state = Object.assign({}, asyncInitialState) + const action = { type: 'ITEMS_FETCHED', items: expectedItems } + const output = asyncReducer(state, action) + + expect(output.items).toEqual(expectedItems) + }) +}) + +describe('NAVIGATE_TO_OLDER', () => { + test('sets beyondPageOne to true', () => { + const state = Object.assign({}, asyncInitialState, { beyondPageOne: false }) + const action = { type: 'NAVIGATE_TO_OLDER' } + const output = asyncReducer(state, action) + + expect(output.beyondPageOne).toEqual(true) + }) +}) diff --git a/apps/block_scout_web/assets/__tests__/pages/address.js b/apps/block_scout_web/assets/__tests__/pages/address.js index ca6f188bef78..98172acceed9 100644 --- a/apps/block_scout_web/assets/__tests__/pages/address.js +++ b/apps/block_scout_web/assets/__tests__/pages/address.js @@ -1,327 +1,62 @@ import { reducer, initialState } from '../../js/pages/address' describe('RECEIVED_NEW_BLOCK', () => { - test('with new block', () => { - const state = Object.assign({}, initialState, { - validationCount: 30, - validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] - }) + test('increases validation count', () => { + const state = Object.assign({}, initialState, { validationCount: 30 }) const action = { type: 'RECEIVED_NEW_BLOCK', - msg: { blockNumber: 2, blockHtml: 'test 2' } + blockHtml: 'test 2' } const output = reducer(state, action) expect(output.validationCount).toEqual(31) - expect(output.validatedBlocks).toEqual([ - { blockNumber: 2, blockHtml: 'test 2' }, - { blockNumber: 1, blockHtml: 'test 1' } - ]) }) - test('when channel has been disconnected', () => { + test('when channel has been disconnected does not increase validation count', () => { const state = Object.assign({}, initialState, { channelDisconnected: true, - validationCount: 30, - validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + validationCount: 30 }) const action = { type: 'RECEIVED_NEW_BLOCK', - msg: { blockNumber: 2, blockHtml: 'test 2' } + blockHtml: 'test 2' } const output = reducer(state, action) expect(output.validationCount).toEqual(30) - expect(output.validatedBlocks).toEqual([ - { blockNumber: 1, blockHtml: 'test 1' } - ]) - }) - test('beyond page one', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - validationCount: 30, - validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_BLOCK', - msg: { blockNumber: 2, blockHtml: 'test 2' } - } - const output = reducer(state, action) - - expect(output.validationCount).toEqual(31) - expect(output.validatedBlocks).toEqual([ - { blockNumber: 1, blockHtml: 'test 1' } - ]) - }) -}) - -describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => { - test('with new internal transaction', () => { - const state = Object.assign({}, initialState, { - internalTransactions: [{ internalTransactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', - msgs: [{ internalTransactionHtml: 'test 2' }] - } - const output = reducer(state, action) - - expect(output.internalTransactions).toEqual([ - { internalTransactionHtml: 'test 2' }, - { internalTransactionHtml: 'test 1' } - ]) - }) - 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.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_INTERNAL_TRANSACTION_BATCH', - msgs: [ - { internalTransactionHtml: 'test 2' } - ] - } - const output = reducer(state, action) - - expect(output.internalTransactionsBatch).toEqual([ - { internalTransactionHtml: 'test 2' }, - { internalTransactionHtml: 'test 1' } - ]) - }) - test('when channel has been disconnected', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true, - internalTransactions: [{ internalTransactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', - msgs: [{ internalTransactionHtml: 'test 2' }] - } - const output = reducer(state, action) - - expect(output.internalTransactions).toEqual([ - { internalTransactionHtml: 'test 1' } - ]) - }) - test('beyond page one', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - internalTransactions: [{ internalTransactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', - msgs: [{ internalTransactionHtml: 'test 2' }] - } - const output = reducer(state, action) - - expect(output.internalTransactions).toEqual([ - { internalTransactionHtml: 'test 1' } - ]) - }) - test('with filtered out internal transaction', () => { - const state = Object.assign({}, initialState, { - filter: 'to' - }) - const action = { - type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', - msgs: [{ internalTransactionHtml: 'test 2' }] - } - const output = reducer(state, action) - - expect(output.internalTransactions).toEqual([]) - }) -}) - -describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { - test('with new pending transaction', () => { - const state = Object.assign({}, initialState, { - pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) - - expect(output.pendingTransactions).toEqual([ - { transactionHash: 2, transactionHtml: 'test 2' }, - { transactionHash: 1, transactionHtml: 'test 1' } - ]) - }) - test('when channel has been disconnected', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true, - pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) - - expect(output.pendingTransactions).toEqual([ - { transactionHash: 1, transactionHtml: 'test 1' } - ]) - }) - test('beyond page one', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) - - expect(output.pendingTransactions).toEqual([ - { transactionHash: 1, transactionHtml: 'test 1' } - ]) - }) - test('with filtered out pending transaction', () => { - const state = Object.assign({}, initialState, { - filter: 'to' - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) - - expect(output.pendingTransactions).toEqual([]) }) }) describe('RECEIVED_NEW_TRANSACTION', () => { - test('with new transaction', () => { + test('increment the transactions count', () => { const state = Object.assign({}, initialState, { - pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], - transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] + addressHash: "0x001", + transactionCount: 1 }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) - 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('when channel has been disconnected', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true, - pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], - transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] - }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } + msg: { fromAddressHash: "0x001", transactionHash: 2, transactionHtml: 'test 2' } } - const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([ - { transactionHash: 2, transactionHtml: 'test' } - ]) - expect(output.transactions).toEqual([ - { transactionHash: 1, transactionHtml: 'test 1' } - ]) - }) - test('beyond page one', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } - } - const output = reducer(state, action) + const newState = reducer(state, action) - expect(output.pendingTransactions).toEqual([]) - expect(output.transactions).toEqual([ - { transactionHash: 1, transactionHtml: 'test 1' } - ]) + expect(newState.transactionCount).toEqual(2) }) - test('with filtered out transaction', () => { + + test('does not increment the count if the channel is disconnected', () => { const state = Object.assign({}, initialState, { - filter: 'to' + addressHash: "0x001", + transactionCount: 1, + channelDisconnected: true }) + const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { transactionHash: 2, transactionHtml: 'test 2' } + msg: { fromAddressHash: "0x001", transactionHash: 2, transactionHtml: 'test 2' } } - const output = reducer(state, action) - - expect(output.transactions).toEqual([]) - }) -}) -describe('RECEIVED_NEXT_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_NEXT_PAGE', - msg: { - nextPageUrl: '2', - transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }] - } - } - const output = reducer(state, action) + const newState = reducer(state, action) - 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(newState.transactionCount).toEqual(1) }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/address/internal_transactions.js b/apps/block_scout_web/assets/__tests__/pages/address/internal_transactions.js new file mode 100644 index 000000000000..516bab5a65b4 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/address/internal_transactions.js @@ -0,0 +1,147 @@ +import { reducer, initialState } from '../../../js/pages/address/internal_transactions' + +describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => { + test('with new internal transaction', () => { + const state = Object.assign({}, initialState, { + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 2', 'test 1']) + }) + + test('with batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + items: ['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.items).toEqual(['test 1']) + expect(output.internalTransactionsBatch).toEqual([ + 'test 13', + 'test 12', + 'test 11', + 'test 10', + 'test 9', + 'test 8', + 'test 7', + 'test 6', + 'test 5', + 'test 4', + 'test 3', + 'test 2', + ]) + }) + + test('after batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + internalTransactionsBatch: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [ + { internalTransactionHtml: 'test 2' } + ] + } + const output = reducer(state, action) + + expect(output.internalTransactionsBatch).toEqual(['test 2', 'test 1']) + }) + + test('when channel has been disconnected', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true, + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 1']) + }) + + test('beyond page one', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 1']) + }) + + test('with filtered "to" internal transaction', () => { + const state = Object.assign({}, initialState, { + filter: 'to', + addressHash: '0x00', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ + fromAddressHash: '0x00', + toAddressHash: '0x01', + internalTransactionHtml: 'test 2' + }, + { + fromAddressHash: '0x01', + toAddressHash: '0x00', + internalTransactionHtml: 'test 3' + }] + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 3']) + }) + + test('with filtered "from" internal transaction', () => { + const state = Object.assign({}, initialState, { + filter: 'from', + addressHash: '0x00', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ + fromAddressHash: '0x00', + toAddressHash: '0x01', + internalTransactionHtml: 'test 2' + }, + { + fromAddressHash: '0x01', + toAddressHash: '0x00', + internalTransactionHtml: 'test 3' + }] + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 2']) + }) +}) diff --git a/apps/block_scout_web/assets/__tests__/pages/address/transactions.js b/apps/block_scout_web/assets/__tests__/pages/address/transactions.js new file mode 100644 index 000000000000..c4676cd6a2fb --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/address/transactions.js @@ -0,0 +1,124 @@ +import { reducer, initialState } from '../../../js/pages/address/transactions' + +describe('RECEIVED_NEW_TRANSACTION', () => { + test('with new transaction', () => { + const state = Object.assign({}, initialState, { + items: ['transaction html'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { transactionHtml: 'another transaction html' } + } + const output = reducer(state, action) + + expect(output.items).toEqual([ 'another transaction html', 'transaction html' ]) + }) + + test('when channel has been disconnected', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true, + items: ['transaction html'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { transactionHtml: 'another transaction html' } + } + const output = reducer(state, action) + + expect(output.items).toEqual(['transaction html']) + }) + + test('beyond page one', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + items: ['transaction html'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { transactionHtml: 'another transaction html' } + } + const output = reducer(state, action) + + expect(output.items).toEqual([ 'transaction html' ]) + }) + + test('adds the new transaction to state even when it is filtered by to', () => { + const state = Object.assign({}, initialState, { + addressHash: '0x001', + filter: 'to', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + fromAddressHash: '0x002', + transactionHtml: 'transaction html', + toAddressHash: '0x001' + } + } + const output = reducer(state, action) + + expect(output.items).toEqual(['transaction html']) + }) + + test( + 'does nothing when it is filtered by to but the toAddressHash is different from addressHash', + () => { + const state = Object.assign({}, initialState, { + addressHash: '0x001', + filter: 'to', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + fromAddressHash: '0x003', + transactionHtml: 'transaction html', + toAddressHash: '0x002' + } + } + const output = reducer(state, action) + + expect(output.items).toEqual([]) + }) + + test('adds the new transaction to state even when it is filtered by from', () => { + const state = Object.assign({}, initialState, { + addressHash: '0x001', + filter: 'from', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + fromAddressHash: '0x001', + transactionHtml: 'transaction html', + toAddressHash: '0x002' + } + } + const output = reducer(state, action) + + expect(output.items).toEqual(['transaction html']) + }) + + test( + 'does nothing when it is filtered by from but the fromAddressHash is different from addressHash', + () => { + const state = Object.assign({}, initialState, { + addressHash: '0x001', + filter: 'to', + items: [] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + addressHash: '0x001', + transactionHtml: 'transaction html', + fromAddressHash: '0x002' + } + } + const output = reducer(state, action) + + expect(output.items).toEqual([]) + }) +}) diff --git a/apps/block_scout_web/assets/__tests__/pages/address/validations.js b/apps/block_scout_web/assets/__tests__/pages/address/validations.js new file mode 100644 index 000000000000..f8bf284029c4 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/address/validations.js @@ -0,0 +1,46 @@ +import { reducer, initialState } from '../../../js/pages/address/validations' + +describe('RECEIVED_NEW_BLOCK', () => { + test('adds new block to the top of the list', () => { + const state = Object.assign({}, initialState, { + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + blockHtml: 'test 2' + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 2', 'test 1']) + }) + + test('does nothing beyond page one', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + channelDisconnected: false, + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + blockHtml: 'test 2' + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 1']) + }) + + test('does nothing when channel has been disconnected', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true, + items: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + blockHtml: 'test 2' + } + const output = reducer(state, action) + + expect(output.items).toEqual(['test 1']) + }) +}) + diff --git a/apps/block_scout_web/assets/__tests__/pages/blocks.js b/apps/block_scout_web/assets/__tests__/pages/blocks.js index f73121614b1a..4d89363ea2ee 100644 --- a/apps/block_scout_web/assets/__tests__/pages/blocks.js +++ b/apps/block_scout_web/assets/__tests__/pages/blocks.js @@ -1,7 +1,7 @@ -import { reducer, initialState, placeHolderBlock } from '../../js/pages/blocks' +import { blockReducer as reducer, initialState, placeHolderBlock } from '../../js/pages/blocks' test('CHANNEL_DISCONNECTED', () => { - const state = initialState + const state = Object.assign({}, initialState, { items: [] }) const action = { type: 'CHANNEL_DISCONNECTED' } @@ -10,158 +10,122 @@ test('CHANNEL_DISCONNECTED', () => { expect(output.channelDisconnected).toBe(true) }) -describe('ELEMENTS_LOAD', () => { - test('page 1 with skipped blocks', () => { - window.localized = {} - const state = Object.assign({}, initialState, { - beyondPageOne: false - }) +describe('RECEIVED_NEW_BLOCK', () => { + test('receives new block', () => { + const state = Object.assign({}, initialState, { items: [], blockType: 'block' }) const action = { - type: 'ELEMENTS_LOAD', - blocks: [ - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 1, blockHtml: 'test 1' } - ] + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: '
' + } } const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 3, blockHtml: placeHolderBlock(3) }, - { blockNumber: 2, blockHtml: placeHolderBlock(2) }, - { blockNumber: 1, blockHtml: 'test 1' } - ]) + expect(output.items).toEqual(['
']) }) - test('page 2 with skipped blocks', () => { - window.localized = {} - const state = Object.assign({}, initialState, { - beyondPageOne: true - }) + test('ignores new block if not in first page', () => { + const state = Object.assign({}, initialState, { items: [], beyondPageOne: true }) const action = { - type: 'ELEMENTS_LOAD', - blocks: [ - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 1, blockHtml: 'test 1' } - ] + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: '
' + } } const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 3, blockHtml: placeHolderBlock(3) }, - { blockNumber: 2, blockHtml: placeHolderBlock(2) }, - { blockNumber: 1, blockHtml: 'test 1' } - ]) + expect(output.items).toEqual([]) }) -}) - -describe('RECEIVED_NEW_BLOCK', () => { - test('receives new block', () => { + test('ignores new block on uncles page', () => { + const state = Object.assign({}, initialState, { items: [], blockType: 'uncle' }) const action = { type: 'RECEIVED_NEW_BLOCK', msg: { - blockHtml: 'test', - blockNumber: 1 + blockHtml: '
' } } - const output = reducer(initialState, action) + const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 1, blockHtml: 'test' } - ]) + expect(output.items).toEqual([]) }) - test('inserts place holders if block received out of order', () => { - window.localized = {} - const state = Object.assign({}, initialState, { - blocks: [ - { blockNumber: 2, blockHtml: 'test 2' } - ] - }) + test('ignores new block on reorgs page', () => { + const state = Object.assign({}, initialState, { items: [], blockType: 'reorg' }) const action = { type: 'RECEIVED_NEW_BLOCK', msg: { - blockHtml: 'test 5', - blockNumber: 5 + blockHtml: '
' } } const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 5, blockHtml: 'test 5' }, - { blockNumber: 4, blockHtml: placeHolderBlock(4) }, - { blockNumber: 3, blockHtml: placeHolderBlock(3) }, - { blockNumber: 2, blockHtml: 'test 2' } - ]) + expect(output.items).toEqual([]) }) - test('replaces duplicated block', () => { + test('inserts place holders if block received out of order', () => { + window.localized = {} const state = Object.assign({}, initialState, { - blocks: [ - { blockNumber: 5, blockHtml: 'test 5' }, - { blockNumber: 4, blockHtml: 'test 4' } - ] + items: [ + '
' + ], + blockType: 'block' }) const action = { type: 'RECEIVED_NEW_BLOCK', msg: { - blockHtml: 'test5', - blockNumber: 5 + blockHtml: '
' } } const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 5, blockHtml: 'test5' }, - { blockNumber: 4, blockHtml: 'test 4' } + expect(output.items).toEqual([ + '
', + placeHolderBlock(4), + placeHolderBlock(3), + '
' ]) }) - test('skips if new block height is lower than lowest on page', () => { + test('replaces duplicated block', () => { const state = Object.assign({}, initialState, { - blocks: [ - { blockNumber: 5, blockHtml: 'test 5' }, - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 3, blockHtml: 'test 3' }, - { blockNumber: 2, blockHtml: 'test 2' } - ] + items: [ + '
', + '
' + ], + blockType: 'block' }) const action = { type: 'RECEIVED_NEW_BLOCK', msg: { - blockNumber: 1, - blockHtml: 'test 1' + blockHtml: '
', } } const output = reducer(state, action) - expect(output.blocks).toEqual([ - { blockNumber: 5, blockHtml: 'test 5' }, - { blockNumber: 4, blockHtml: 'test 4' }, - { blockNumber: 3, blockHtml: 'test 3' }, - { blockNumber: 2, blockHtml: 'test 2' } + expect(output.items).toEqual([ + '
', + '
' ]) }) -}) - -describe('RECEIVED_NEXT_PAGE', () => { - test('with new block page', () => { + test('skips if new block height is lower than lowest on page', () => { const state = Object.assign({}, initialState, { - loadingNextPage: true, - nextPageUrl: '1', - blocks: [{ blockNumber: 2, blockHtml: 'test 2' }] + items: [ + '
', + '
', + '
', + '
' + ], + blockType: 'block' }) const action = { - type: 'RECEIVED_NEXT_PAGE', + type: 'RECEIVED_NEW_BLOCK', msg: { - nextPageUrl: '2', - blocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + blockHtml: '
' } } const output = reducer(state, action) - expect(output.loadingNextPage).toEqual(false) - expect(output.nextPageUrl).toEqual('2') - expect(output.blocks).toEqual([ - { blockNumber: 2, blockHtml: 'test 2' }, - { blockNumber: 1, blockHtml: 'test 1' } + expect(output.items).toEqual([ + '
', + '
', + '
', + '
' ]) }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js index a34f1a7600bd..e9f7fc941d7c 100644 --- a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js +++ b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js @@ -13,7 +13,7 @@ test('CHANNEL_DISCONNECTED', () => { describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { test('single transaction', () => { - const state = initialState + const state = Object.assign({}, initialState, {items:[]}) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: [{ @@ -23,15 +23,12 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([{ - transactionHash: '0x00', - transactionHtml: 'test' - }]) + expect(output.items).toEqual(['test']) expect(output.pendingTransactionsBatch.length).toEqual(0) expect(output.pendingTransactionCount).toEqual(1) }) test('large batch of transactions', () => { - const state = initialState + const state = Object.assign({}, initialState, {items:[]}) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: [{ @@ -71,37 +68,32 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.pendingTransactionsBatch.length).toEqual(11) expect(output.pendingTransactionCount).toEqual(11) }) test('single transaction after single transaction', () => { const state = Object.assign({}, initialState, { - pendingTransactions: [{ - transactionHash: '0x01', - transactionHtml: 'test 1' - }], + items: ['test 0x01'], pendingTransactionCount: 1 }) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: [{ transactionHash: '0x02', - transactionHtml: 'test 2' + transactionHtml: 'test 0x02' }] } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([ - { transactionHash: '0x02', transactionHtml: 'test 2' }, - { transactionHash: '0x01', transactionHtml: 'test 1' } - ]) + expect(output.items).toEqual(['test 0x02', 'test 0x01']) expect(output.pendingTransactionsBatch.length).toEqual(0) expect(output.pendingTransactionCount).toEqual(2) }) test('single transaction after large batch of transactions', () => { const state = Object.assign({}, initialState, { - pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] + pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'], + items: [] }) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', @@ -112,12 +104,13 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.pendingTransactionsBatch.length).toEqual(12) }) test('large batch of transactions after large batch of transactions', () => { const state = Object.assign({}, initialState, { - pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] + pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'], + items: [] }) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', @@ -158,12 +151,13 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.pendingTransactionsBatch.length).toEqual(22) }) test('after disconnection', () => { const state = Object.assign({}, initialState, { - channelDisconnected: true + channelDisconnected: true, + items: [] }) const action = { type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', @@ -174,7 +168,7 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.pendingTransactions).toEqual([]) + expect(output.items).toEqual([]) }) }) @@ -182,41 +176,24 @@ describe('RECEIVED_NEW_TRANSACTION', () => { test('single transaction collated', () => { const state = Object.assign({}, initialState, { pendingTransactionCount: 2, - pendingTransactions: [{ - transactionHash: '0x00', - transactionHtml: 'old' - }] + items: ['old 0x00'] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', msg: { transactionHash: '0x00', - transactionHtml: 'new' + transactionHtml: 'new 0x00' } } const output = reducer(state, action) expect(output.pendingTransactionCount).toBe(1) - expect(output.pendingTransactions).toEqual([{ - transactionHash: '0x00', - transactionHtml: 'new' - }]) + expect(output.items).toEqual(['new 0x00']) }) test('single transaction collated after batch', () => { const state = Object.assign({}, initialState, { - pendingTransactionsBatch: [ - { transactionHash: '0x01' }, - { transactionHash: '2' }, - { transactionHash: '3' }, - { transactionHash: '4' }, - { transactionHash: '5' }, - { transactionHash: '6' }, - { transactionHash: '7' }, - { transactionHash: '8' }, - { transactionHash: '9' }, - { transactionHash: '10' }, - { transactionHash: '11' } - ] + pendingTransactionsBatch: ['0x01', '0x02', '0x03', '0x04', '0x05', '0x06', '0x07', '0x08', '0x09', '0x0a', '0x0b'], + items: [] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', @@ -227,6 +204,6 @@ describe('RECEIVED_NEW_TRANSACTION', () => { const output = reducer(state, action) expect(output.pendingTransactionsBatch.length).toEqual(10) - expect(_.map(output.pendingTransactionsBatch, 'transactionHash')).not.toContain('0x01') + expect(output.pendingTransactionsBatch).not.toContain('0x01') }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/transactions.js b/apps/block_scout_web/assets/__tests__/pages/transactions.js index e1cef7aaa4f1..de5fe06b6072 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transactions.js +++ b/apps/block_scout_web/assets/__tests__/pages/transactions.js @@ -13,76 +13,63 @@ test('CHANNEL_DISCONNECTED', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { test('single transaction', () => { - const state = initialState + const state = Object.assign({}, initialState, { items: [] }) const action = { type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: [{ - transactionHtml: 'test' + transactionHtml: 'transaction_html' }] } const output = reducer(state, action) - expect(output.transactions).toEqual([{ transactionHtml: 'test' }]) + expect(output.items).toEqual(['transaction_html']) expect(output.transactionsBatch.length).toEqual(0) expect(output.transactionCount).toEqual(1) }) + test('large batch of transactions', () => { - const state = initialState + const state = Object.assign({}, initialState, { items: [] }) 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' - }] + msgs: [ + { transactionHtml: 'transaction_html_1' }, + { transactionHtml: 'transaction_html_2' }, + { transactionHtml: 'transaction_html_3' }, + { transactionHtml: 'transaction_html_4' }, + { transactionHtml: 'transaction_html_5' }, + { transactionHtml: 'transaction_html_6' }, + { transactionHtml: 'transaction_html_7' }, + { transactionHtml: 'transaction_html_8' }, + { transactionHtml: 'transaction_html_9' }, + { transactionHtml: 'transaction_html_10' }, + { transactionHtml: 'transaction_html_11' }, + ] } const output = reducer(state, action) - expect(output.transactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.transactionsBatch.length).toEqual(11) expect(output.transactionCount).toEqual(11) }) + test('single transaction after single transaction', () => { - const state = Object.assign({}, initialState, { - transactions: [{ - transactionHtml: 'test 1' - }] - }) + const state = Object.assign({}, initialState, { items: [ 'transaction_html' ] }) const action = { type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: [{ - transactionHtml: 'test 2' + transactionHtml: 'another_transaction_html' }] } const output = reducer(state, action) - expect(output.transactions).toEqual([ - { transactionHtml: 'test 2' }, - { transactionHtml: 'test 1' } - ]) + expect(output.items).toEqual([ 'another_transaction_html', 'transaction_html' ]) expect(output.transactionsBatch.length).toEqual(0) }) + test('single transaction after large batch of transactions', () => { const state = Object.assign({}, initialState, { - transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11] + items: [], + transactionsBatch: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }) const action = { type: 'RECEIVED_NEW_TRANSACTION_BATCH', @@ -92,57 +79,51 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { } const output = reducer(state, action) - expect(output.transactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.transactionsBatch.length).toEqual(12) }) + test('large batch of transactions after large batch of transactions', () => { const state = Object.assign({}, initialState, { - transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11] + items: [], + transactionsBatch: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 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' - }] + msgs: [ + { transactionHtml: 'transaction_html_12' }, + { transactionHtml: 'transaction_html_13' }, + { transactionHtml: 'transaction_html_14' }, + { transactionHtml: 'transaction_html_15' }, + { transactionHtml: 'transaction_html_16' }, + { transactionHtml: 'transaction_html_17' }, + { transactionHtml: 'transaction_html_18' }, + { transactionHtml: 'transaction_html_19' }, + { transactionHtml: 'transaction_html_20' }, + { transactionHtml: 'transaction_html_21' }, + { transactionHtml: 'transaction_html_22' } + ] } const output = reducer(state, action) - expect(output.transactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.transactionsBatch.length).toEqual(22) }) + test('after disconnection', () => { const state = Object.assign({}, initialState, { + items: [], channelDisconnected: true }) const action = { type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: [{ - transactionHtml: 'test' + transactionHtml: 'transaction_html' }] } const output = reducer(state, action) - expect(output.transactions).toEqual([]) + expect(output.items).toEqual([]) expect(output.transactionsBatch.length).toEqual(0) }) }) diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index 3c722e8ec980..a3be02359c67 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -47,6 +47,9 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "node_modules/bootstrap/scss/badge"; @import "node_modules/bootstrap/scss/alert"; +// Code highlight +@import "node_modules/highlight.js/styles/default"; + //Custom theme @import "theme/fonts"; @@ -82,6 +85,8 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "components/dropdown"; @import "components/loading-spinner"; @import "components/transaction-input"; +@import "components/coin-balance-tile"; +@import "components/highlight"; :export { primary: $primary; diff --git a/apps/block_scout_web/assets/css/components/_coin-balance-tile.scss b/apps/block_scout_web/assets/css/components/_coin-balance-tile.scss new file mode 100644 index 000000000000..d8d782be5943 --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_coin-balance-tile.scss @@ -0,0 +1,9 @@ +.tile.tile-type-coin-balance { + [data-balance-change-sign="Positive"] { + color: $green; + } + + [data-balance-change-sign="Negative"] { + color: $red; + } +} diff --git a/apps/block_scout_web/assets/css/components/_highlight.scss b/apps/block_scout_web/assets/css/components/_highlight.scss new file mode 100644 index 000000000000..c504be04f28c --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_highlight.scss @@ -0,0 +1,19 @@ +//replace the default background color from highlightjs +.hljs { + background: $gray-100; +} + +.line-numbers { + + [data-line-number] { + + &:before { + content: attr(data-line-number); + display: inline-block; + border-right: 1px solid $gray-400; + padding: 0 .5em; + margin-right: .5em; + color: $gray-600 + } + } +} diff --git a/apps/block_scout_web/assets/css/components/_nav_tabs.scss b/apps/block_scout_web/assets/css/components/_nav_tabs.scss index 90e4d0e7365a..8010f4b214d2 100644 --- a/apps/block_scout_web/assets/css/components/_nav_tabs.scss +++ b/apps/block_scout_web/assets/css/components/_nav_tabs.scss @@ -1,7 +1,7 @@ .nav-tabs { .nav-link { - padding: 1.25rem 3rem; + padding: 1.25rem 2.5rem; color: $text-muted; font-size: 14px; border-top-left-radius: 0; diff --git a/apps/block_scout_web/assets/css/components/_tile.scss b/apps/block_scout_web/assets/css/components/_tile.scss index 7013626273a9..a70282384a98 100644 --- a/apps/block_scout_web/assets/css/components/_tile.scss +++ b/apps/block_scout_web/assets/css/components/_tile.scss @@ -81,6 +81,13 @@ .tile-label { color: $orange; } + + &-short-name { + overflow: hidden; + max-width: 45%; + vertical-align: middle; + text-overflow: ellipsis; + } } &-unique-token { diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index 8ff4aa19f974..af5281327950 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -21,13 +21,15 @@ import 'bootstrap' import './locale' import './pages/address' +import './pages/address/coin_balances' +import './pages/address/transactions' +import './pages/address/validations' +import './pages/address/internal_transactions' import './pages/blocks' import './pages/chain' import './pages/pending_transactions' -import './pages/reorgs' import './pages/transaction' import './pages/transactions' -import './pages/uncles' import './lib/clipboard_buttons' import './lib/currency' @@ -38,6 +40,7 @@ import './lib/market_history_chart' import './lib/pending_transactions_toggle' import './lib/pretty_json' import './lib/reload_button' +import './lib/smart_contract/code_highlighting' import './lib/smart_contract/read_only_functions' import './lib/smart_contract/wei_ether_converter' import './lib/stop_propagation' diff --git a/apps/block_scout_web/assets/js/lib/async_listing_load.js b/apps/block_scout_web/assets/js/lib/async_listing_load.js index f2ffe5fdb7c2..2e9e4e371b91 100644 --- a/apps/block_scout_web/assets/js/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/js/lib/async_listing_load.js @@ -1,85 +1,223 @@ import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import listMorph from '../lib/list_morph' +import reduceReducers from 'reduce-reducers' +import { createStore, connectElements } from '../lib/redux_helpers.js' + /** - * This script is a generic function to load list within a tab async. See token transfers tab at Token's page as example. + * This is a generic lib to add pagination with asynchronous page loading. There are two ways of + * activating this in a page. + * + * If the page has no redux associated with, all you need is a markup with the following pattern: + * + *
+ *
message
+ *
message
+ *
message
+ *
+ * button text + *
loading text
+ *
+ * + * the data-async-load is the attribute responsible for binding the store. + * + * If the page has a redux associated with, you need to connect the reducers instead of creating + * the store using the `createStore`. For instance: * - * To get it working the markup must follow the pattern below: + * // my_page.js + * const initialState = { ... } + * const reducer = (state, action) => { ... } + * const store = createAsyncLoadStore(reducer, initialState, 'item.Key') * - *
- *
message
- *
message
- *
message
- *
- * button text - *
loading text
- *
+ * The createAsyncLoadStore function will return a store with asynchronous loading activated. This + * approach will expect the same markup above, except for data-async-load attribute, which is used + * to create a store and it is not necessary for this case. * */ -const $element = $('[data-async-listing]') - -function asyncListing (element, path) { - const $mainElement = $(element) - const $items = $mainElement.find('[data-items]') - const $loading = $mainElement.find('[data-loading-message]') - const $nextPageButton = $mainElement.find('[data-next-page-button]') - const $loadingButton = $mainElement.find('[data-loading-button]') - const $errorMessage = $mainElement.find('[data-error-message]') - const $emptyResponseMessage = $mainElement.find('[data-empty-response-message]') - - $.getJSON(path, {type: 'JSON'}) - .done(response => { - if (!response.items || response.items.length === 0) { - $emptyResponseMessage.show() - $items.empty() - } else { - $items.html(response.items) + +export const asyncInitialState = { + /* it will consider any query param in the current URI as paging */ + beyondPageOne: (URI(window.location).query() !== ''), + /* an array with every html element of the list being shown */ + items: [], + /* the key for diffing the elements in the items array */ + itemKey: null, + /* represents whether a request is happening or not */ + loading: false, + /* if there was an error fetching items */ + requestError: false, + /* if it is loading the first page */ + loadingFirstPage: true, + /* link to the next page */ + nextPagePath: null +} + +export function asyncReducer (state = asyncInitialState, action) { + switch (action.type) { + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, { nextPagePath: action.nextPagePath }) + } + case 'ADD_ITEM_KEY': { + return Object.assign({}, state, { itemKey: action.itemKey }) + } + case 'START_REQUEST': { + return Object.assign({}, state, { + loading: true, + requestError: false + }) + } + case 'REQUEST_ERROR': { + return Object.assign({}, state, { requestError: true }) + } + case 'FINISH_REQUEST': { + return Object.assign({}, state, { + loading: false, + loadingFirstPage: false + }) + } + case 'ITEMS_FETCHED': { + return Object.assign({}, state, { + requestError: false, + items: action.items, + nextPagePath: action.nextPagePath + }) + } + case 'NAVIGATE_TO_OLDER': { + history.replaceState({}, null, state.nextPagePath) + + return Object.assign({}, state, { beyondPageOne: true }) + } + default: + return state + } +} + +export const elements = { + '[data-async-listing]': { + load ($el) { + const nextPagePath = $el.data('async-listing') + + return { nextPagePath } + } + }, + '[data-async-listing] [data-loading-message]': { + render ($el, state) { + if (state.loadingFirstPage) return $el.show() + + $el.hide() + } + }, + '[data-async-listing] [data-empty-response-message]': { + render ($el, state) { + if ( + !state.requestError && + (!state.loading || !state.loadingFirstPage) && + state.items.length === 0 + ) { + return $el.show() } - if (response.next_page_path) { - $nextPageButton.attr('href', response.next_page_path) - $nextPageButton.show() - } else { - $nextPageButton.hide() + + $el.hide() + } + }, + '[data-async-listing] [data-error-message]': { + render ($el, state) { + if (state.requestError) return $el.show() + + $el.hide() + } + }, + '[data-async-listing] [data-items]': { + render ($el, state, oldState) { + if (state.items === oldState.items) return + + if (state.itemKey) { + const container = $el[0] + const newElements = _.map(state.items, (item) => $(item)[0]) + listMorph(container, newElements, { key: state.itemKey }) + return } - }) - .fail(() => $errorMessage.show()) - .always(() => { - $loading.hide() - $loadingButton.hide() - }) -} -if ($element.length === 1) { - $element.on('click', '[data-next-page-button]', (event) => { - event.preventDefault() + $el.html(state.items) + } + }, + '[data-async-listing] [data-next-page-button]': { + render ($el, state) { + if (state.requestError) return $el.hide() + if (!state.nextPagePath) return $el.hide() + if (state.loading) return $el.hide() - const $button = $(event.target) - const path = $button.attr('href') - const $loadingButton = $element.find('[data-loading-button]') + $el.show() + $el.attr('href', state.nextPagePath) + } + }, + '[data-async-listing] [data-loading-button]': { + render ($el, state) { + if (!state.loadingFirstPage && state.loading) return $el.show() - // change url to the next page link before loading the next page - history.pushState({}, null, path) - $button.hide() - $loadingButton.show() + $el.hide() + } + } +} - asyncListing($element, path) - }) +/** + * Create a store combining the given reducer and initial state with the async reducer. + * + * reducer: The reducer that will be merged with the asyncReducer to add async + * loading capabilities to a page. Any state changes in the reducer passed will be + * applied AFTER the asyncReducer. + * + * initialState: The initial state to be merged with the async state. Any state + * values passed here will overwrite the values on asyncInitialState. + * + * itemKey: it will be added to the state as the key for diffing the elements and + * adding or removing with the correct animation. Check list_morph.js for more informantion. + */ +export function createAsyncLoadStore (reducer, initialState, itemKey) { + const state = _.merge(asyncInitialState, initialState) + const store = createStore(reduceReducers(asyncReducer, reducer, state)) - $element.on('click', '[data-error-message]', (event) => { - event.preventDefault() + if (typeof itemKey !== 'undefined') { + store.dispatch({ + type: 'ADD_ITEM_KEY', + itemKey + }) + } - // event.target had a weird behavior here - // it hid the tag but left the red div showing - const $link = $element.find('[data-error-message]') - const $loading = $element.find('[data-loading-message]') - const path = $element.data('async-listing') + connectElements({store, elements}) + firstPageLoad(store) + return store +} - $link.hide() - $loading.show() +function firstPageLoad (store) { + const $element = $('[data-async-listing]') + function loadItems () { + const path = store.getState().nextPagePath + store.dispatch({type: 'START_REQUEST'}) + $.getJSON(path, {type: 'JSON'}) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) + } + loadItems() - asyncListing($element, path) + $element.on('click', '[data-error-message]', (event) => { + event.preventDefault() + loadItems() }) - // force browser to reload when the user goes back a page - $(window).on('popstate', () => location.reload()) + $element.on('click', '[data-next-page-button]', (event) => { + event.preventDefault() + loadItems() + store.dispatch({type: 'NAVIGATE_TO_OLDER'}) + }) +} - asyncListing($element, $element.data('async-listing')) +const $element = $('[data-async-load]') +if ($element.length) { + const store = createStore(asyncReducer) + connectElements({store, elements}) + firstPageLoad(store) } diff --git a/apps/block_scout_web/assets/js/lib/coin_balance_history_chart.js b/apps/block_scout_web/assets/js/lib/coin_balance_history_chart.js new file mode 100644 index 000000000000..e18553897c7f --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/coin_balance_history_chart.js @@ -0,0 +1,60 @@ +import $ from 'jquery' +import Chart from 'chart.js' +import humps from 'humps' + +export function createCoinBalanceHistoryChart (el) { + const $chartContainer = $('[data-chart-container]') + const $chartLoading = $('[data-chart-loading-message]') + const $chartError = $('[data-chart-error-message]') + const dataPath = el.dataset.coin_balance_history_data_path + + $.getJSON(dataPath, {type: 'JSON'}) + .done(data => { + $chartContainer.show() + + const coinBalanceHistoryData = humps.camelizeKeys(data) + .map(balance => ({ + x: balance.date, + y: balance.value + })) + + return new Chart(el, { + type: 'line', + data: { + datasets: [{ + label: 'coin balance', + data: coinBalanceHistoryData + }] + }, + options: { + legend: { + display: false + }, + scales: { + xAxes: [{ + type: 'time', + time: { + unit: 'day', + stepSize: 3 + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + }, + scaleLabel: { + display: true, + labelString: window.localized['Ether'] + } + }] + } + } + }) + }) + .fail(() => { + $chartError.show() + }) + .always(() => { + $chartLoading.hide() + }) +} diff --git a/apps/block_scout_web/assets/js/lib/market_history_chart.js b/apps/block_scout_web/assets/js/lib/market_history_chart.js index c863b6aa515a..db6c1128761f 100644 --- a/apps/block_scout_web/assets/js/lib/market_history_chart.js +++ b/apps/block_scout_web/assets/js/lib/market_history_chart.js @@ -121,9 +121,9 @@ class MarketHistoryChart { } } -export function createMarketHistoryChart (ctx) { - const availableSupply = JSON.parse(ctx.dataset.available_supply) - const marketHistoryData = humps.camelizeKeys(JSON.parse(ctx.dataset.market_history_data)) +export function createMarketHistoryChart (el) { + const availableSupply = JSON.parse(el.dataset.available_supply) + const marketHistoryData = humps.camelizeKeys(JSON.parse(el.dataset.market_history_data)) - return new MarketHistoryChart(ctx, availableSupply, marketHistoryData) + return new MarketHistoryChart(el, availableSupply, marketHistoryData) } diff --git a/apps/block_scout_web/assets/js/lib/redux_helpers.js b/apps/block_scout_web/assets/js/lib/redux_helpers.js index e45fa7d51692..fdf7c659b375 100644 --- a/apps/block_scout_web/assets/js/lib/redux_helpers.js +++ b/apps/block_scout_web/assets/js/lib/redux_helpers.js @@ -2,10 +2,99 @@ import $ from 'jquery' import _ from 'lodash' import { createStore as reduxCreateStore } from 'redux' +/** + * Create a redux store given the reducer. It also enables the Redux dev tools. + */ export function createStore (reducer) { return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) } +/** + * Connect elements with the redux store. It must receive an object with the following attributes: + * + * elements: It is an object with elements that are going to react to the redux state or add something + * to the initial state. + * + * ```javascript + * const elements = { + * // The JQuery selector for finding elements in the page. + * '[data-counter]': { + * // Useful to put things from the page to the redux state. + * load ($element) {...}, + * // Check for state changes and manipulates the DOM accordingly. + * render ($el, state, oldState) {...} + * } + * } + * ``` + * + * The load and render functions are optional, you can have both or just one of them. It depends + * on if you want to load something to the state in the first render and/or that the element should + * react to the redux state. Notice that you can include more elements if you want to since elements + * also is an object. + * + * store: It is the redux store that the elements should be connected with. + * ```javascript + * const store = createStore(reducer) + * ``` + * + * action: The first action that the store is going to dispatch. Optional, by default 'ELEMENTS_LOAD' + * is going to be dispatched. + * + * ### Examples + * + * Given the markup: + * ```HTML + *
+ * 1 + *
+ * ``` + * + * The reducer: + * ```javascript + * function reducer (state = { number: null }, action) { + * switch (action.type) { + * case 'ELEMENTS_LOAD': { + * return Object.assign({}, state, { number: action.number }) + * } + * case 'INCREMENT': { + * return Object.assign({}, state, { number: state.number + 1 }) + * } + * default: + * return state + * } + * } + * ``` + * + * The elements: + * ```javascript + * const elements = { + * // '[data-counter]' is the element that will be connected to the redux store. + * '[data-counter]': { + * // Find the number within data-counter and add to the state. + * load ($el) { + * return { number: $el.find('.number').val() } + * }, + * // React to redux state. Case the number in the state changes, it is going to render the + * // new number. + * render ($el, state, oldState) { + * if (state.number === oldState.number) return + * + * $el.html(state.number) + * } + * } + * } + * + * All we need to do is connecting the store and the elements using this function. + * ```javascript + * connectElements({store, elements}) + * ``` + * + * Now, if we dispatch the `INCREMENT` action, the state is going to change and the [data-counter] + * element is going to re-render since they are connected. + * ```javascript + * store.dispatch({type: 'INCREMENT'}) + * ``` + */ export function connectElements ({ elements, store, action = 'ELEMENTS_LOAD' }) { function loadElements () { return _.reduce(elements, (pageLoadParams, { load }, selector) => { @@ -16,6 +105,7 @@ export function connectElements ({ elements, store, action = 'ELEMENTS_LOAD' }) return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams }, {}) } + function renderElements (state, oldState) { _.forIn(elements, ({ render }, selector) => { if (!render) return @@ -24,11 +114,13 @@ export function connectElements ({ elements, store, action = 'ELEMENTS_LOAD' }) render($el, state, oldState) }) } + let oldState = store.getState() store.subscribe(() => { const state = store.getState() renderElements(state, oldState) oldState = state }) + store.dispatch(Object.assign(loadElements(), { type: action })) } diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/code_highlighting.js b/apps/block_scout_web/assets/js/lib/smart_contract/code_highlighting.js new file mode 100644 index 000000000000..95320a12a67c --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/smart_contract/code_highlighting.js @@ -0,0 +1,9 @@ +import $ from 'jquery' +import hljs from 'highlight.js' +import hljsDefineSolidity from 'highlightjs-solidity' + +// only activate highlighting on pages with this selector +if ($('[data-activate-highlight]').length > 0) { + hljsDefineSolidity(hljs) + hljs.initHighlightingOnLoad() +} diff --git a/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js index 3fac9f8ae809..97d9ddc20519 100644 --- a/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js +++ b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js @@ -17,4 +17,3 @@ const tokenBalanceDropdown = (element) => { export function loadTokenBalanceDropdown () { $('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) } -loadTokenBalanceDropdown() diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 8472a2130ee5..857bb1c2159e 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -3,17 +3,11 @@ import _ from 'lodash' import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' -import socket from '../socket' +import socket, { subscribeChannel } from '../socket' import { createStore, connectElements } from '../lib/redux_helpers.js' -import { batchChannel } from '../lib/utils' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' -import listMorph from '../lib/list_morph' 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 = { channelDisconnected: false, @@ -22,22 +16,10 @@ export const initialState = { balance: null, transactionCount: null, - validationCount: null, - - pendingTransactions: [], - transactions: [], - internalTransactions: [], - internalTransactionsBatch: [], - validatedBlocks: [], - - beyondPageOne: null, - - nextPageUrl: $('[data-selector="transactions-list"]').length ? URI(window.location).addQuery({ type: 'JSON' }).toString() : null + validationCount: null } -export const reducer = withInfiniteScroll(baseReducer) - -function baseReducer (state = initialState, action) { +export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': case 'ELEMENTS_LOAD': { @@ -47,103 +29,27 @@ function baseReducer (state = initialState, action) { if (state.beyondPageOne) return state return Object.assign({}, state, { - channelDisconnected: true, - internalTransactionsBatch: [] + channelDisconnected: true }) } case 'RECEIVED_NEW_BLOCK': { if (state.channelDisconnected) return state const validationCount = state.validationCount + 1 - - if (state.beyondPageOne) return Object.assign({}, state, { validationCount }) - return Object.assign({}, state, { - validatedBlocks: [ - action.msg, - ...state.validatedBlocks - ], - validationCount - }) - } - case 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH': { - if (state.channelDisconnected || state.beyondPageOne) return state - - const incomingInternalTransactions = action.msgs - .filter(({toAddressHash, fromAddressHash}) => ( - !state.filter || - (state.filter === 'to' && toAddressHash === state.addressHash) || - (state.filter === 'from' && fromAddressHash === state.addressHash) - )) - - if (!state.internalTransactionsBatch.length && incomingInternalTransactions.length < BATCH_THRESHOLD) { - return Object.assign({}, state, { - internalTransactions: [ - ...incomingInternalTransactions.reverse(), - ...state.internalTransactions - ] - }) - } else { - return Object.assign({}, state, { - internalTransactionsBatch: [ - ...incomingInternalTransactions.reverse(), - ...state.internalTransactionsBatch - ] - }) - } - } - case 'RECEIVED_NEW_PENDING_TRANSACTION': { - if (state.channelDisconnected || state.beyondPageOne) return state - - if ((state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) || - (state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) { - return state - } - - return Object.assign({}, state, { - pendingTransactions: [ - action.msg, - ...state.pendingTransactions - ] - }) - } - case 'REMOVE_PENDING_TRANSACTION': { - return Object.assign({}, state, { - pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash) - }) + return Object.assign({}, state, { validationCount }) } case 'RECEIVED_NEW_TRANSACTION': { if (state.channelDisconnected) return state const transactionCount = (action.msg.fromAddressHash === state.addressHash) ? state.transactionCount + 1 : state.transactionCount - if (state.beyondPageOne || - (state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) || - (state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) { - return Object.assign({}, state, { transactionCount }) - } - - return Object.assign({}, state, { - pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? Object.assign({}, action.msg, { validated: true }) : transaction), - transactions: [ - action.msg, - ...state.transactions - ], - transactionCount: transactionCount - }) + return Object.assign({}, state, { transactionCount }) } case 'RECEIVED_UPDATED_BALANCE': { return Object.assign({}, state, { balance: action.msg.balance }) } - case 'RECEIVED_NEXT_PAGE': { - return Object.assign({}, state, { - transactions: [ - ...state.transactions, - ...action.msg.transactions - ] - }) - } default: return state } @@ -183,99 +89,6 @@ const elements = { if (oldState.validationCount === state.validationCount) return $el.empty().append(numeral(state.validationCount).format()) } - }, - '[data-selector="pending-transactions-list"]': { - load ($el) { - return { - pendingTransactions: $el.children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray() - } - }, - 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="empty-transactions-list"]': { - render ($el, state) { - if (state.transactions.length || state.loadingNextPage || state.pagingError) { - $el.hide() - } else { - $el.show() - } - } - }, - '[data-selector="transactions-list"]': { - load ($el) { - return { - transactions: $el.children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray() - } - }, - 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() - } - }, - 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' }) - } } } @@ -291,10 +104,9 @@ if ($addressDetailsPage.length) { beyondPageOne: !!blockNumber }) connectElements({ store, elements }) - $('[data-selector="transactions-list"]').length && connectInfiniteScroll(store) - const addressChannel = socket.channel(`addresses:${addressHash}`, {}) - addressChannel.join() + const addressChannel = subscribeChannel(`addresses:${addressHash}`) + addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) @@ -302,23 +114,11 @@ if ($addressDetailsPage.length) { 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}`, {}) diff --git a/apps/block_scout_web/assets/js/pages/address/coin_balances.js b/apps/block_scout_web/assets/js/pages/address/coin_balances.js new file mode 100644 index 000000000000..8c0100d3d264 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/address/coin_balances.js @@ -0,0 +1,69 @@ +import $ from 'jquery' +import _ from 'lodash' +import humps from 'humps' +import socket from '../../socket' +import { connectElements } from '../../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../../lib/async_listing_load' +import { createCoinBalanceHistoryChart } from '../../lib/coin_balance_history_chart' + +export const initialState = { + channelDisconnected: false +} + +export function reducer (state, action) { + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'CHANNEL_DISCONNECTED': { + if (state.beyondPageOne) return state + + return Object.assign({}, state, { + channelDisconnected: true + }) + } + case 'RECEIVED_NEW_COIN_BALANCE': { + if (state.channelDisconnected || state.beyondPageOne) return state + + return Object.assign({}, state, { + items: [action.msg.coinBalanceHtml, ...state.items] + }) + } + default: + return state + } +} + +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + } +} + +if ($('[data-page="coin-balance-history"]').length) { + const store = createAsyncLoadStore(reducer, initialState, 'dataset.blockNumber') + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + + store.dispatch({type: 'PAGE_LOAD', addressHash}) + connectElements({ store, elements }) + + const addressChannel = socket.channel(`addresses:${addressHash}`, {}) + addressChannel.join() + addressChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + addressChannel.on('coin_balance', (msg) => { + store.dispatch({ + type: 'RECEIVED_NEW_COIN_BALANCE', + msg: humps.camelizeKeys(msg) + }) + }) + + const chartContainer = $('[data-chart="coinBalanceHistoryChart"]')[0] + if (chartContainer) { + createCoinBalanceHistoryChart(chartContainer) + } +} diff --git a/apps/block_scout_web/assets/js/pages/address/internal_transactions.js b/apps/block_scout_web/assets/js/pages/address/internal_transactions.js new file mode 100644 index 000000000000..aa95278c71d9 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/address/internal_transactions.js @@ -0,0 +1,96 @@ +import $ from 'jquery' +import _ from 'lodash' +import humps from 'humps' +import numeral from 'numeral' +import socket from '../../socket' +import { batchChannel } from '../../lib/utils' +import { connectElements } from '../../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../../lib/async_listing_load' + +const BATCH_THRESHOLD = 10 + +export const initialState = { + channelDisconnected: false, + addressHash: null, + filter: null, + internalTransactionsBatch: [] +} + +export function reducer (state, action) { + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'CHANNEL_DISCONNECTED': { + if (state.beyondPageOne) return state + + return Object.assign({}, state, { + channelDisconnected: true, + internalTransactionsBatch: [] + }) + } + case 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH': { + if (state.channelDisconnected || state.beyondPageOne) return state + + const incomingInternalTransactions = action.msgs + .filter(({toAddressHash, fromAddressHash}) => ( + !state.filter || + (state.filter === 'to' && toAddressHash === state.addressHash) || + (state.filter === 'from' && fromAddressHash === state.addressHash) + )).map(msg => msg.internalTransactionHtml) + + if (!state.internalTransactionsBatch.length && incomingInternalTransactions.length < BATCH_THRESHOLD) { + return Object.assign({}, state, { + items: [ + ...incomingInternalTransactions.reverse(), + ...state.items + ] + }) + } else { + return Object.assign({}, state, { + internalTransactionsBatch: [ + ...incomingInternalTransactions.reverse(), + ...state.internalTransactionsBatch + ] + }) + } + } + default: + return state + } +} + +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + }, + '[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() + } + } +} + +if ($('[data-page="address-internal-transactions"]').length) { + const store = createAsyncLoadStore(reducer, initialState, 'dataset.key') + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + + store.dispatch({type: 'PAGE_LOAD', addressHash}) + connectElements({ store, elements }) + + const addressChannel = socket.channel(`addresses:${addressHash}`, {}) + addressChannel.join() + addressChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + addressChannel.on('internal_transaction', batchChannel((msgs) => store.dispatch({ + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: humps.camelizeKeys(msgs) + }))) +} diff --git a/apps/block_scout_web/assets/js/pages/address/transactions.js b/apps/block_scout_web/assets/js/pages/address/transactions.js new file mode 100644 index 000000000000..3388e3ced16f --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/address/transactions.js @@ -0,0 +1,73 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import { subscribeChannel } from '../../socket' +import { connectElements } from '../../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../../lib/async_listing_load' + +export const initialState = { + addressHash: null, + channelDisconnected: false, + filter: null +} + +export function reducer (state, action) { + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'CHANNEL_DISCONNECTED': { + if (state.beyondPageOne) return state + + return Object.assign({}, state, { channelDisconnected: true }) + } + case 'RECEIVED_NEW_TRANSACTION': { + if (state.channelDisconnected) return state + + if (state.beyondPageOne || + (state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) || + (state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) { + return state + } + + return Object.assign({}, state, { items: [ action.msg.transactionHtml, ...state.items ] }) + } + default: + return state + } +} + +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + } +} + +if ($('[data-page="address-transactions"]').length) { + const store = createAsyncLoadStore(reducer, initialState, 'dataset.transactionHash') + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) + + connectElements({ store, elements }) + + store.dispatch({ + type: 'PAGE_LOAD', + addressHash, + filter, + beyondPageOne: !!blockNumber + }) + + const addressChannel = subscribeChannel(`addresses:${addressHash}`) + + addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + addressChannel.on('transaction', (msg) => { + store.dispatch({ + type: 'RECEIVED_NEW_TRANSACTION', + msg: humps.camelizeKeys(msg) + }) + }) +} diff --git a/apps/block_scout_web/assets/js/pages/address/validations.js b/apps/block_scout_web/assets/js/pages/address/validations.js new file mode 100644 index 000000000000..6aca07a98b32 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/address/validations.js @@ -0,0 +1,64 @@ +import $ from 'jquery' +import _ from 'lodash' +import humps from 'humps' +import socket from '../../socket' +import { connectElements } from '../../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../../lib/async_listing_load.js' + +export const initialState = { + addressHash: null, + channelDisconnected: false +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'CHANNEL_DISCONNECTED': { + return Object.assign({}, state, { channelDisconnected: true }) + } + case 'RECEIVED_NEW_BLOCK': { + if (state.channelDisconnected) return state + if (state.beyondPageOne) return state + + return Object.assign({}, state, { + items: [ + action.blockHtml, + ...state.items + ] + }) + } + default: + return state + } +} + +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + } +} + +if ($('[data-page="blocks-validated"]').length) { + const store = createAsyncLoadStore(reducer, initialState, 'dataset.blockNumber') + connectElements({ store, elements }) + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + store.dispatch({ + type: 'PAGE_LOAD', + addressHash + }) + + 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', + blockHtml: humps.camelizeKeys(msg).blockHtml + })) +} diff --git a/apps/block_scout_web/assets/js/pages/blocks.js b/apps/block_scout_web/assets/js/pages/blocks.js index 585110add830..f494c6b518f3 100644 --- a/apps/block_scout_web/assets/js/pages/blocks.js +++ b/apps/block_scout_web/assets/js/pages/blocks.js @@ -2,17 +2,14 @@ import $ from 'jquery' import _ from 'lodash' import humps from 'humps' import socket from '../socket' -import { createStore, connectElements } from '../lib/redux_helpers.js' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' -import listMorph from '../lib/list_morph' +import { connectElements } from '../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../lib/async_listing_load' export const initialState = { - channelDisconnected: false, - - blocks: [] + channelDisconnected: false } -export const reducer = withMissingBlocks(withInfiniteScroll(baseReducer)) +export const blockReducer = withMissingBlocks(baseReducer) function baseReducer (state = initialState, action) { switch (action.type) { @@ -25,27 +22,15 @@ function baseReducer (state = initialState, action) { }) } case 'RECEIVED_NEW_BLOCK': { - if (state.channelDisconnected) return state - - if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) { - return Object.assign({}, state, { - blocks: [ - action.msg, - ...state.blocks - ] - }) - } else { - return Object.assign({}, state, { - blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block) - }) - } - } - case 'RECEIVED_NEXT_PAGE': { + if (state.channelDisconnected || state.beyondPageOne || state.blockType !== 'block') return state + + const blockNumber = getBlockNumber(action.msg.blockHtml) + const minBlock = getBlockNumber(_.last(state.items)) + + if (state.items.length && blockNumber < minBlock) return state + return Object.assign({}, state, { - blocks: [ - ...state.blocks, - ...action.msg.blocks - ] + items: [action.msg.blockHtml, ...state.items] }) } default: @@ -53,54 +38,52 @@ function baseReducer (state = initialState, action) { } } +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + } +} + +function getBlockNumber (blockHtml) { + return $(blockHtml).data('blockNumber') +} + function withMissingBlocks (reducer) { return (...args) => { const result = reducer(...args) - if (result.blocks.length < 2) return result + if (result.items.length < 2) return result - const maxBlock = _.first(result.blocks).blockNumber - const minBlock = _.last(result.blocks).blockNumber + const maxBlock = getBlockNumber(_.first(result.items)) + const minBlock = getBlockNumber(_.last(result.items)) + + const blockNumbersToItems = result.items.reduce((acc, item) => { + const blockNumber = getBlockNumber(item) + acc[blockNumber] = acc[blockNumber] || item + return acc + }, {}) return Object.assign({}, result, { - blocks: _.rangeRight(minBlock, maxBlock + 1) - .map((blockNumber) => _.find(result.blocks, ['blockNumber', blockNumber]) || { - blockNumber, - blockHtml: placeHolderBlock(blockNumber) - }) + items: _.rangeRight(minBlock, maxBlock + 1) + .map((blockNumber) => blockNumbersToItems[blockNumber] || placeHolderBlock(blockNumber)) }) } } -const elements = { - '[data-selector="channel-disconnected-message"]': { - render ($el, state) { - if (state.channelDisconnected) $el.show() - } - }, - '[data-selector="blocks-list"]': { - load ($el) { - return { - blocks: $el.children().map((index, el) => ({ - blockNumber: parseInt(el.dataset.blockNumber), - blockHtml: el.outerHTML - })).toArray() - } - }, - render ($el, state, oldState) { - if (oldState.blocks === state.blocks) return - const container = $el[0] - const newElements = _.map(state.blocks, ({ blockHtml }) => $(blockHtml)[0]) - listMorph(container, newElements, { key: 'dataset.blockNumber' }) - } - } -} - const $blockListPage = $('[data-page="block-list"]') -if ($blockListPage.length) { - const store = createStore(reducer) +const $uncleListPage = $('[data-page="uncle-list"]') +const $reorgListPage = $('[data-page="reorg-list"]') +if ($blockListPage.length || $uncleListPage.length || $reorgListPage.length) { + const blockType = $blockListPage.length ? 'block' : $uncleListPage.length ? 'uncle' : 'reorg' + + const store = createAsyncLoadStore( + $blockListPage.length ? blockReducer : baseReducer, + Object.assign({}, initialState, { blockType }), + 'dataset.blockNumber' + ) connectElements({ store, elements }) - connectInfiniteScroll(store) const blocksChannel = socket.channel(`blocks:new_block`, {}) blocksChannel.join() diff --git a/apps/block_scout_web/assets/js/pages/pending_transactions.js b/apps/block_scout_web/assets/js/pages/pending_transactions.js index 90f37a6182d8..8025cea6584d 100644 --- a/apps/block_scout_web/assets/js/pages/pending_transactions.js +++ b/apps/block_scout_web/assets/js/pages/pending_transactions.js @@ -3,10 +3,9 @@ import _ from 'lodash' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { createStore, connectElements } from '../lib/redux_helpers.js' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' +import { connectElements } from '../lib/redux_helpers.js' import { batchChannel } from '../lib/utils' -import listMorph from '../lib/list_morph' +import { createAsyncLoadStore } from '../lib/async_listing_load' const BATCH_THRESHOLD = 10 @@ -15,13 +14,10 @@ export const initialState = { pendingTransactionCount: null, - pendingTransactions: [], pendingTransactionsBatch: [] } -export const reducer = withInfiniteScroll(baseReducer) - -function baseReducer (state = initialState, action) { +export function reducer (state = initialState, action) { switch (action.type) { case 'ELEMENTS_LOAD': { return Object.assign({}, state, _.omit(action, 'type')) @@ -33,10 +29,9 @@ function baseReducer (state = initialState, action) { } case 'RECEIVED_NEW_TRANSACTION': { if (state.channelDisconnected) return state - return Object.assign({}, state, { - pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? action.msg : transaction), - pendingTransactionsBatch: state.pendingTransactionsBatch.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash), + items: state.items.map((item) => item.includes(action.msg.transactionHash) ? action.msg.transactionHtml : item), + pendingTransactionsBatch: state.pendingTransactionsBatch.filter(transactionHtml => !transactionHtml.includes(action.msg.transactionHash)), pendingTransactionCount: state.pendingTransactionCount - 1 }) } @@ -44,19 +39,20 @@ function baseReducer (state = initialState, action) { if (state.channelDisconnected) return state const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length + const pendingTransactionHtml = action.msgs.map(message => message.transactionHtml) if (!state.pendingTransactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) { return Object.assign({}, state, { - pendingTransactions: [ - ...action.msgs.reverse(), - ...state.pendingTransactions + items: [ + ...pendingTransactionHtml.reverse(), + ...state.items ], pendingTransactionCount }) } else { return Object.assign({}, state, { pendingTransactionsBatch: [ - ...action.msgs.reverse(), + ...pendingTransactionHtml.reverse(), ...state.pendingTransactionsBatch ], pendingTransactionCount @@ -65,15 +61,7 @@ function baseReducer (state = initialState, action) { } case 'REMOVE_PENDING_TRANSACTION': { return Object.assign({}, state, { - pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash) - }) - } - case 'RECEIVED_NEXT_PAGE': { - return Object.assign({}, state, { - pendingTransactions: [ - ...state.pendingTransactions, - ...action.msg.pendingTransactions - ] + items: state.items.filter(transactionHtml => !transactionHtml.includes(action.msg.transactionHash)) }) } default: @@ -106,30 +94,13 @@ const elements = { if (oldState.transactionCount === state.transactionCount) return $el.empty().append(numeral(state.transactionCount).format()) } - }, - '[data-selector="transactions-pending-list"]': { - load ($el) { - return { - pendingTransactions: $el.children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray() - } - }, - 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' }) - } } } const $transactionPendingListPage = $('[data-page="transaction-pending-list"]') if ($transactionPendingListPage.length) { - const store = createStore(reducer) + const store = createAsyncLoadStore(reducer, initialState, 'dataset.transactionHash') connectElements({ store, elements }) - connectInfiniteScroll(store) const transactionsChannel = socket.channel(`transactions:new_transaction`) transactionsChannel.join() diff --git a/apps/block_scout_web/assets/js/pages/reorgs.js b/apps/block_scout_web/assets/js/pages/reorgs.js deleted file mode 100644 index 618ffcaac8f9..000000000000 --- a/apps/block_scout_web/assets/js/pages/reorgs.js +++ /dev/null @@ -1,52 +0,0 @@ -import $ from 'jquery' -import _ from 'lodash' -import { createStore, connectElements } from '../lib/redux_helpers.js' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' -import listMorph from '../lib/list_morph' - -export const initialState = { - reorgs: [] -} - -export const reducer = withInfiniteScroll(baseReducer) - -function baseReducer (state = initialState, action) { - switch (action.type) { - case 'ELEMENTS_LOAD': { - return Object.assign({}, state, _.omit(action, 'type')) - } - case 'RECEIVED_NEXT_PAGE': { - return Object.assign({}, state, { - reorgs: [ - ...state.reorgs, - ..._.map(action.msg.blocks, 'blockHtml') - ] - }) - } - default: - return state - } -} - -const elements = { - '[data-selector="blocks-list"]': { - load ($el) { - return { - reorgs: _.map($el.children().toArray(), 'outerHTML') - } - }, - render ($el, state, oldState) { - if (oldState.reorgs === state.reorgs) return - const container = $el[0] - const newElements = state.reorgs.map((html) => $(html)[0]) - listMorph(container, newElements, { key: 'dataset.blockHash' }) - } - } -} - -const $reorgListPage = $('[data-page="reorg-list"]') -if ($reorgListPage.length) { - const store = createStore(reducer) - connectElements({ store, elements }) - connectInfiniteScroll(store) -} diff --git a/apps/block_scout_web/assets/js/pages/transactions.js b/apps/block_scout_web/assets/js/pages/transactions.js index 3e435f60f093..e23836ca3679 100644 --- a/apps/block_scout_web/assets/js/pages/transactions.js +++ b/apps/block_scout_web/assets/js/pages/transactions.js @@ -3,25 +3,19 @@ import _ from 'lodash' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { createStore, connectElements } from '../lib/redux_helpers.js' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' +import { connectElements } from '../lib/redux_helpers' +import { createAsyncLoadStore } from '../lib/async_listing_load' import { batchChannel } from '../lib/utils' -import listMorph from '../lib/list_morph' const BATCH_THRESHOLD = 10 export const initialState = { channelDisconnected: false, - transactionCount: null, - - transactions: [], transactionsBatch: [] } -export const reducer = withInfiniteScroll(baseReducer) - -function baseReducer (state = initialState, action) { +export function reducer (state = initialState, action) { switch (action.type) { case 'ELEMENTS_LOAD': { return Object.assign({}, state, _.omit(action, 'type')) @@ -33,15 +27,15 @@ function baseReducer (state = initialState, action) { }) } case 'RECEIVED_NEW_TRANSACTION_BATCH': { - if (state.channelDisconnected) return state + if (state.channelDisconnected || state.beyondPageOne) return state const transactionCount = state.transactionCount + action.msgs.length if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) { return Object.assign({}, state, { - transactions: [ - ...action.msgs.reverse(), - ...state.transactions + items: [ + ...action.msgs.map(msg => msg.transactionHtml).reverse(), + ...state.items ], transactionCount }) @@ -55,14 +49,6 @@ function baseReducer (state = initialState, action) { }) } } - case 'RECEIVED_NEXT_PAGE': { - return Object.assign({}, state, { - transactions: [ - ...state.transactions, - ...action.msg.transactions - ] - }) - } default: return state } @@ -90,30 +76,14 @@ const elements = { if (oldState.transactionCount === state.transactionCount) return $el.empty().append(numeral(state.transactionCount).format()) } - }, - '[data-selector="transactions-list"]': { - load ($el, store) { - return { - transactions: $el.children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray() - } - }, - render ($el, state, oldState) { - if (oldState.transactions === state.transactions) return - const container = $el[0] - const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0]) - listMorph(container, newElements, { key: 'dataset.transactionHash' }) - } } } const $transactionListPage = $('[data-page="transaction-list"]') if ($transactionListPage.length) { - const store = createStore(reducer) + const store = createAsyncLoadStore(reducer, initialState, 'dataset.transactionHash') + connectElements({ store, elements }) - connectInfiniteScroll(store) const transactionsChannel = socket.channel(`transactions:new_transaction`) transactionsChannel.join() diff --git a/apps/block_scout_web/assets/js/pages/uncles.js b/apps/block_scout_web/assets/js/pages/uncles.js deleted file mode 100644 index b9652173e985..000000000000 --- a/apps/block_scout_web/assets/js/pages/uncles.js +++ /dev/null @@ -1,52 +0,0 @@ -import $ from 'jquery' -import _ from 'lodash' -import { createStore, connectElements } from '../lib/redux_helpers.js' -import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers' -import listMorph from '../lib/list_morph' - -export const initialState = { - uncles: [] -} - -export const reducer = withInfiniteScroll(baseReducer) - -function baseReducer (state = initialState, action) { - switch (action.type) { - case 'ELEMENTS_LOAD': { - return Object.assign({}, state, _.omit(action, 'type')) - } - case 'RECEIVED_NEXT_PAGE': { - return Object.assign({}, state, { - uncles: [ - ...state.uncles, - ..._.map(action.msg.blocks, 'blockHtml') - ] - }) - } - default: - return state - } -} - -const elements = { - '[data-selector="blocks-list"]': { - load ($el) { - return { - uncles: _.map($el.children().toArray(), 'outerHTML') - } - }, - render ($el, state, oldState) { - if (oldState.uncles === state.uncles) return - const container = $el[0] - const newElements = state.uncles.map((html) => $(html)[0]) - listMorph(container, newElements, { key: 'dataset.blockHash' }) - } - } -} - -const $uncleListPage = $('[data-page="uncle-list"]') -if ($uncleListPage.length) { - const store = createStore(reducer) - connectElements({ store, elements }) - connectInfiniteScroll(store) -} diff --git a/apps/block_scout_web/assets/js/socket.js b/apps/block_scout_web/assets/js/socket.js index 89bbae11062f..956345930dd2 100644 --- a/apps/block_scout_web/assets/js/socket.js +++ b/apps/block_scout_web/assets/js/socket.js @@ -5,3 +5,26 @@ const socket = new Socket('/socket', {params: {locale: locale}}) socket.connect() export default socket + +/** + * Subscribes the client in the channel given the topic. + * + * This function will check if already exist a channel before creating one. This is useful because + * when the client is attempting to create a duplicated subscription, the server will close the + * existing subscription and create a new one. + * + * See more about it in https://hexdocs.pm/phoenix/js/#phoenix. + * + * Returns a Channel instance. + */ +export function subscribeChannel (topic) { + const channel = socket.channels.find(channel => channel.topic === topic) + + if (channel) { + return channel + } else { + const channel = socket.channel(topic, {}) + channel.join() + return channel + } +} diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index a9a49fb7c128..f531a1f5d5da 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -4,23 +4,31 @@ "lockfileVersion": 1, "dependencies": { "@babel/code-frame": { - "version": "7.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz", - "integrity": "sha1-vXHZsZKvl435FYKdOdQJRFZDmgw=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", "dev": true, "requires": { - "@babel/highlight": "7.0.0-beta.51" + "@babel/highlight": "^7.0.0" } }, "@babel/highlight": { - "version": "7.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.51.tgz", - "integrity": "sha1-6IRK4loVlcz9QriWI7Q3bKBtIl0=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", - "js-tokens": "^3.0.0" + "js-tokens": "^4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } } }, "@babel/polyfill": { @@ -290,9 +298,9 @@ } }, "abab": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", "dev": true }, "abbrev": { @@ -317,12 +325,21 @@ } }, "acorn-globals": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", - "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", + "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", "dev": true, "requires": { - "acorn": "^5.0.0" + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + } } }, "acorn-jsx": { @@ -342,6 +359,12 @@ } } }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -360,17 +383,6 @@ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "dev": true }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", @@ -432,12 +444,12 @@ } }, "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "^1.0.0" } }, "aproba": { @@ -883,9 +895,9 @@ } }, "babel-jest": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.2.0.tgz", - "integrity": "sha1-FKnWo/QSLf6mBp03CFrfJqU6Tbo=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", "dev": true, "requires": { "babel-plugin-istanbul": "^4.1.6", @@ -1531,9 +1543,9 @@ "dev": true }, "browser-process-hrtime": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", - "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", "dev": true }, "browser-resolve": { @@ -1730,13 +1742,6 @@ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", @@ -1806,17 +1811,6 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -1897,9 +1891,9 @@ } }, "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "dev": true }, "cipher-base": { @@ -2002,27 +1996,6 @@ "tiny-emitter": "^2.0.0" } }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -2049,12 +2022,6 @@ } } }, - "clorox": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clorox/-/clorox-1.0.3.tgz", - "integrity": "sha512-w3gKAUKMJYmmaJyc+p+iDrDtLvsFasrx/y6/zWo2U1TZfsz3y4Vl4T9PHCZrOwk1eMTOSRI6xHdpDR4PhTdy8Q==", - "dev": true - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2159,18 +2126,19 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true, + "optional": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "compare-versions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", - "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", - "dev": true - }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -2637,9 +2605,9 @@ "dev": true }, "cssstyle": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.3.1.tgz", - "integrity": "sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", + "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", "dev": true, "requires": { "cssom": "0.3.x" @@ -2670,14 +2638,27 @@ } }, "data-urls": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", - "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", "dev": true, "requires": { - "abab": "^1.0.4", - "whatwg-mimetype": "^2.0.0", - "whatwg-url": "^6.4.0" + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } } }, "date-now": { @@ -2714,22 +2695,32 @@ "dev": true }, "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", "dev": true, "requires": { - "strip-bom": "^3.0.0" + "strip-bom": "^2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } } }, "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" + "object-keys": "^1.0.12" } }, "define-property": { @@ -3016,14 +3007,14 @@ } }, "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { - "is-callable": "^1.1.1", + "is-callable": "^1.1.4", "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" + "is-symbol": "^1.0.2" } }, "escape-string-regexp": { @@ -3033,9 +3024,9 @@ "dev": true }, "escodegen": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.10.0.tgz", - "integrity": "sha512-fjUOf8johsv23WuIKdNQU4P9t9jhQ4Qzx6pC2uW890OloK3Zs1ZAoCNpg/2larNF501jLl3UNy0kIRcF6VI22g==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", "dev": true, "requires": { "esprima": "^3.1.3", @@ -3367,12 +3358,6 @@ "strip-eof": "^1.0.0" } }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3408,18 +3393,60 @@ } } }, + "expand-range": { + "version": "1.8.2", + "resolved": "http://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, "expect": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.2.0.tgz", - "integrity": "sha1-U6fhNeNv4n51hnsReP8IqqzCsN0=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", + "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", "dev": true, "requires": { "ansi-styles": "^3.2.0", - "jest-diff": "^23.2.0", + "jest-diff": "^23.6.0", "jest-get-type": "^22.1.0", - "jest-matcher-utils": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0" + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0" } }, "extend": { @@ -3611,6 +3638,12 @@ "schema-utils": "^0.4.5" } }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -3707,12 +3740,6 @@ "for-in": "^1.0.1" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -4480,6 +4507,42 @@ "path-is-absolute": "^1.0.0" } }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -4559,32 +4622,15 @@ "dev": true }, "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", + "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", "dev": true, "requires": { - "async": "^1.4.0", + "async": "^2.5.0", "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" } }, "har-schema": { @@ -4627,6 +4673,12 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -4685,6 +4737,16 @@ "minimalistic-assert": "^1.0.0" } }, + "highlight.js": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz", + "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==" + }, + "highlightjs-solidity": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-1.0.6.tgz", + "integrity": "sha512-NzdwI5gX+8H3z/YEXk01dKOY0QuffhNkUZw9umHUCXlzKB+1n2SexTTZpSGAmZYetHT/bccCm+3QqBULtTLmdA==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -4962,12 +5024,12 @@ "dev": true }, "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dev": true, "requires": { - "ci-info": "^1.0.0" + "ci-info": "^1.5.0" } }, "is-data-descriptor": { @@ -5010,6 +5072,21 @@ "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", "dev": true }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -5100,6 +5177,18 @@ "isobject": "^3.0.1" } }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -5137,10 +5226,13 @@ } }, "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } }, "is-typedarray": { "version": "1.0.0", @@ -5184,45 +5276,25 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", - "dev": true, - "requires": { - "async": "^2.1.4", - "compare-versions": "^3.1.0", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-hook": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-report": "^1.1.4", - "istanbul-lib-source-maps": "^1.2.4", - "istanbul-reports": "^1.3.0", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - } - }, "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", "dev": true }, "istanbul-lib-hook": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", - "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "^0.4.0" } }, "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", "dev": true, "requires": { "babel-generator": "^6.18.0", @@ -5230,17 +5302,17 @@ "babel-traverse": "^6.18.0", "babel-types": "^6.18.0", "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "semver": "^5.3.0" } }, "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "mkdirp": "^0.5.1", "path-parse": "^1.0.5", "supports-color": "^3.1.2" @@ -5263,40 +5335,10 @@ } } }, - "istanbul-lib-source-maps": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", - "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.0", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", "dev": true, "requires": { "handlebars": "^4.0.3" @@ -5318,10 +5360,84 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, "jest-cli": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.2.0.tgz", - "integrity": "sha1-O1Q6PaUUXdiTeTEBcoI3n8aWxFs=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", + "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", "dev": true, "requires": { "ansi-escapes": "^3.0.0", @@ -5335,22 +5451,22 @@ "istanbul-lib-coverage": "^1.2.0", "istanbul-lib-instrument": "^1.10.1", "istanbul-lib-source-maps": "^1.2.4", - "jest-changed-files": "^23.2.0", - "jest-config": "^23.2.0", - "jest-environment-jsdom": "^23.2.0", + "jest-changed-files": "^23.4.2", + "jest-config": "^23.6.0", + "jest-environment-jsdom": "^23.4.0", "jest-get-type": "^22.1.0", - "jest-haste-map": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0", - "jest-resolve-dependencies": "^23.2.0", - "jest-runner": "^23.2.0", - "jest-runtime": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", - "jest-watcher": "^23.2.0", + "jest-haste-map": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0", + "jest-resolve-dependencies": "^23.6.0", + "jest-runner": "^23.6.0", + "jest-runtime": "^23.6.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "jest-watcher": "^23.4.0", "jest-worker": "^23.2.0", - "micromatch": "^3.1.10", + "micromatch": "^2.3.11", "node-notifier": "^5.2.1", "prompts": "^0.1.9", "realpath-native": "^1.0.0", @@ -5360,6 +5476,385 @@ "strip-ansi": "^4.0.0", "which": "^1.2.12", "yargs": "^11.0.0" + }, + "dependencies": { + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "istanbul-api": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", + "dev": true, + "requires": { + "async": "^2.1.4", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + } + }, + "jest-changed-files": { + "version": "23.4.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", + "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", + "dev": true, + "requires": { + "throat": "^4.0.0" + } + }, + "jest-config": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", + "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", + "dev": true, + "requires": { + "babel-core": "^6.0.0", + "babel-jest": "^23.6.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^23.4.0", + "jest-environment-node": "^23.4.0", + "jest-get-type": "^22.1.0", + "jest-jasmine2": "^23.6.0", + "jest-regex-util": "^23.3.0", + "jest-resolve": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "micromatch": "^2.3.11", + "pretty-format": "^23.6.0" + } + }, + "jest-environment-jsdom": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", + "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", + "dev": true, + "requires": { + "jest-mock": "^23.2.0", + "jest-util": "^23.4.0", + "jsdom": "^11.5.1" + } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "http://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "jest-haste-map": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", + "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", + "dev": true, + "requires": { + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.11", + "invariant": "^2.2.4", + "jest-docblock": "^23.2.0", + "jest-serializer": "^23.0.1", + "jest-worker": "^23.2.0", + "micromatch": "^2.3.11", + "sane": "^2.0.0" + } + }, + "jest-message-util": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", + "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0-beta.35", + "chalk": "^2.0.1", + "micromatch": "^2.3.11", + "slash": "^1.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-regex-util": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", + "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", + "dev": true + }, + "jest-resolve-dependencies": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", + "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", + "dev": true, + "requires": { + "jest-regex-util": "^23.3.0", + "jest-snapshot": "^23.6.0" + } + }, + "jest-runner": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", + "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", + "dev": true, + "requires": { + "exit": "^0.1.2", + "graceful-fs": "^4.1.11", + "jest-config": "^23.6.0", + "jest-docblock": "^23.2.0", + "jest-haste-map": "^23.6.0", + "jest-jasmine2": "^23.6.0", + "jest-leak-detector": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-runtime": "^23.6.0", + "jest-util": "^23.4.0", + "jest-worker": "^23.2.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + } + }, + "jest-runtime": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", + "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", + "dev": true, + "requires": { + "babel-core": "^6.0.0", + "babel-plugin-istanbul": "^4.1.6", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "exit": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.11", + "jest-config": "^23.6.0", + "jest-haste-map": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0", + "jest-resolve": "^23.6.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "micromatch": "^2.3.11", + "realpath-native": "^1.0.0", + "slash": "^1.0.0", + "strip-bom": "3.0.0", + "write-file-atomic": "^2.1.0", + "yargs": "^11.0.0" + } + }, + "jest-snapshot": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", + "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", + "dev": true, + "requires": { + "babel-types": "^6.0.0", + "chalk": "^2.0.1", + "jest-diff": "^23.6.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-resolve": "^23.6.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^23.6.0", + "semver": "^5.5.0" + } + }, + "jest-util": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", + "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", + "dev": true, + "requires": { + "callsites": "^2.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.11", + "is-ci": "^1.0.10", + "jest-message-util": "^23.4.0", + "mkdirp": "^0.5.1", + "slash": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "jest-validate": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.6.0" + } + }, + "jest-watcher": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", + "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "string-length": "^2.0.0" + } + }, + "jest-worker": { + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", + "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", + "dev": true, + "requires": { + "merge-stream": "^1.0.1" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "node-notifier": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", + "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", + "dev": true, + "requires": { + "growly": "^1.3.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "prompts": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", + "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", + "dev": true, + "requires": { + "kleur": "^2.0.1", + "sisteransi": "^0.1.1" + } + }, + "realpath-native": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", + "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } + }, + "string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "strip-ansi": { @@ -5373,46 +5868,16 @@ } } }, - "jest-changed-files": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.2.0.tgz", - "integrity": "sha1-oUWm5LZtASn8fJnO4TTck3pkPZw=", - "dev": true, - "requires": { - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.2.0.tgz", - "integrity": "sha1-0vtVb9WioZw561bROdzKXa0qHIg=", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-jest": "^23.2.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^23.2.0", - "jest-environment-node": "^23.2.0", - "jest-get-type": "^22.1.0", - "jest-jasmine2": "^23.2.0", - "jest-regex-util": "^23.0.0", - "jest-resolve": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", - "pretty-format": "^23.2.0" - } - }, "jest-diff": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.2.0.tgz", - "integrity": "sha1-nyz0tR4Sx5FVAgCrwWtHEwrxBio=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", "dev": true, "requires": { "chalk": "^2.0.1", "diff": "^3.2.0", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-docblock": { @@ -5425,34 +5890,23 @@ } }, "jest-each": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.2.0.tgz", - "integrity": "sha1-pAD4HIVwg/UMT1M5mxCfEgI/sZ0=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", + "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", "dev": true, "requires": { "chalk": "^2.0.1", - "pretty-format": "^23.2.0" - } - }, - "jest-environment-jsdom": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.2.0.tgz", - "integrity": "sha1-NjRgOgipdbDKimWDIPVqVKjgRVg=", - "dev": true, - "requires": { - "jest-mock": "^23.2.0", - "jest-util": "^23.2.0", - "jsdom": "^11.5.1" + "pretty-format": "^23.6.0" } }, "jest-environment-node": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.2.0.tgz", - "integrity": "sha1-tv5BNy44IJO7bz2b32wcTsClDxg=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", + "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", "dev": true, "requires": { "jest-mock": "^23.2.0", - "jest-util": "^23.2.0" + "jest-util": "^23.4.0" } }, "jest-get-type": { @@ -5461,71 +5915,139 @@ "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", "dev": true }, - "jest-haste-map": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.2.0.tgz", - "integrity": "sha1-0Qy6wAfGlZSMjvGCGisu0tTy1Ng=", - "dev": true, - "requires": { - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.1.11", - "jest-docblock": "^23.2.0", - "jest-serializer": "^23.0.1", - "jest-worker": "^23.2.0", - "micromatch": "^3.1.10", - "sane": "^2.0.0" - } - }, "jest-jasmine2": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.2.0.tgz", - "integrity": "sha1-qmcM2x5NX47HdMlN2l4QX+M9i7Q=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", + "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", "dev": true, "requires": { + "babel-traverse": "^6.0.0", "chalk": "^2.0.1", "co": "^4.6.0", - "expect": "^23.2.0", + "expect": "^23.6.0", "is-generator-fn": "^1.0.0", - "jest-diff": "^23.2.0", - "jest-each": "^23.2.0", - "jest-matcher-utils": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", - "pretty-format": "^23.2.0" + "jest-diff": "^23.6.0", + "jest-each": "^23.6.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "pretty-format": "^23.6.0" } }, "jest-leak-detector": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.2.0.tgz", - "integrity": "sha1-wonZYdxjjxQ1fU75bgQx7MGqN30=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", + "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", "dev": true, "requires": { - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-matcher-utils": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.2.0.tgz", - "integrity": "sha1-TUmB8jIT6Tnjzt8j3DTHR7WuGRM=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "^2.0.1", "jest-get-type": "^22.1.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0" } }, "jest-message-util": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.2.0.tgz", - "integrity": "sha1-WR6BSP/2nPibBBSAnHIXVuvv50Q=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", + "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", "dev": true, "requires": { "@babel/code-frame": "^7.0.0-beta.35", "chalk": "^2.0.1", - "micromatch": "^3.1.10", + "micromatch": "^2.3.11", "slash": "^1.0.0", "stack-utils": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + } } }, "jest-mock": { @@ -5535,15 +6057,15 @@ "dev": true }, "jest-regex-util": { - "version": "23.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.0.0.tgz", - "integrity": "sha1-3Vwf3gxG9DcTFM8Q96dRoj9Oj3Y=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", + "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", "dev": true }, "jest-resolve": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.2.0.tgz", - "integrity": "sha1-oHkK1aO5kAKrTb/L+Nni1qabPZk=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", + "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", "dev": true, "requires": { "browser-resolve": "^1.11.3", @@ -5551,78 +6073,6 @@ "realpath-native": "^1.0.0" } }, - "jest-resolve-dependencies": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.2.0.tgz", - "integrity": "sha1-bfjVcJxkBmOc0H9Uv/B04BtcBFg=", - "dev": true, - "requires": { - "jest-regex-util": "^23.0.0", - "jest-snapshot": "^23.2.0" - } - }, - "jest-runner": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.2.0.tgz", - "integrity": "sha1-DZGWfqgvcrDHBZEJJghtIFXOda8=", - "dev": true, - "requires": { - "exit": "^0.1.2", - "graceful-fs": "^4.1.11", - "jest-config": "^23.2.0", - "jest-docblock": "^23.2.0", - "jest-haste-map": "^23.2.0", - "jest-jasmine2": "^23.2.0", - "jest-leak-detector": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-runtime": "^23.2.0", - "jest-util": "^23.2.0", - "jest-worker": "^23.2.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - }, - "dependencies": { - "source-map-support": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", - "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } - } - }, - "jest-runtime": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.2.0.tgz", - "integrity": "sha1-YtywF2ahxMZGltwJAgnnbOGq3Lw=", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-plugin-istanbul": "^4.1.6", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "exit": "^0.1.2", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.11", - "jest-config": "^23.2.0", - "jest-haste-map": "^23.2.0", - "jest-message-util": "^23.2.0", - "jest-regex-util": "^23.0.0", - "jest-resolve": "^23.2.0", - "jest-snapshot": "^23.2.0", - "jest-util": "^23.2.0", - "jest-validate": "^23.2.0", - "micromatch": "^3.1.10", - "realpath-native": "^1.0.0", - "slash": "^1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "^2.1.0", - "yargs": "^11.0.0" - } - }, "jest-serializer": { "version": "23.0.1", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-23.0.1.tgz", @@ -5630,30 +6080,34 @@ "dev": true }, "jest-snapshot": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.2.0.tgz", - "integrity": "sha1-x6PQFxd7utYMillYac+QqHguan4=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", + "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", "dev": true, "requires": { + "babel-types": "^6.0.0", "chalk": "^2.0.1", - "jest-diff": "^23.2.0", - "jest-matcher-utils": "^23.2.0", + "jest-diff": "^23.6.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-resolve": "^23.6.0", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^23.2.0" + "pretty-format": "^23.6.0", + "semver": "^5.5.0" } }, "jest-util": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.2.0.tgz", - "integrity": "sha1-YrdwdXaW2W4JSgS48cNzylClqy4=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", + "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", "dev": true, "requires": { "callsites": "^2.0.0", "chalk": "^2.0.1", "graceful-fs": "^4.1.11", "is-ci": "^1.0.10", - "jest-message-util": "^23.2.0", + "jest-message-util": "^23.4.0", "mkdirp": "^0.5.1", "slash": "^1.0.0", "source-map": "^0.6.0" @@ -5667,38 +6121,6 @@ } } }, - "jest-validate": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.2.0.tgz", - "integrity": "sha1-Z8i5CeEa8XAXZSOIlMZ6wykbGV4=", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.2.0" - } - }, - "jest-watcher": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.2.0.tgz", - "integrity": "sha1-Z46FKJbpGenZoOtLi68a4nliDqk=", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "string-length": "^2.0.0" - } - }, - "jest-worker": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", - "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", - "dev": true, - "requires": { - "merge-stream": "^1.0.1" - } - }, "jquery": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", @@ -5733,36 +6155,36 @@ "optional": true }, "jsdom": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.11.0.tgz", - "integrity": "sha512-ou1VyfjwsSuWkudGxb03FotDajxAto6USAlmMZjE2lc0jCznt7sBWkhfRBRaWwbnmDqdMSTKTLT5d9sBFkkM7A==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", "dev": true, "requires": { - "abab": "^1.0.4", - "acorn": "^5.3.0", + "abab": "^2.0.0", + "acorn": "^5.5.3", "acorn-globals": "^4.1.0", "array-equal": "^1.0.0", "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": ">= 0.3.1 < 0.4.0", + "cssstyle": "^1.0.0", "data-urls": "^1.0.0", - "domexception": "^1.0.0", - "escodegen": "^1.9.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.2.0", - "nwsapi": "^2.0.0", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", "parse5": "4.0.0", "pn": "^1.1.0", - "request": "^2.83.0", + "request": "^2.87.0", "request-promise-native": "^1.0.5", "sax": "^1.2.4", "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.3", + "tough-cookie": "^2.3.4", "w3c-hr-time": "^1.0.1", "webidl-conversions": "^4.0.2", "whatwg-encoding": "^1.0.3", "whatwg-mimetype": "^2.1.0", "whatwg-url": "^6.4.1", - "ws": "^4.0.0", + "ws": "^5.2.0", "xml-name-validator": "^3.0.0" } }, @@ -5829,12 +6251,11 @@ "is-buffer": "^1.1.5" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true + "kleur": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", + "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", + "dev": true }, "lcid": { "version": "1.0.0", @@ -5985,12 +6406,6 @@ "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", "dev": true }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, "loose-envify": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", @@ -6070,6 +6485,12 @@ "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", "dev": true }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -6205,9 +6626,9 @@ } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, "merge-stream": { @@ -6540,18 +6961,6 @@ } } }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "semver": "^5.4.1", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, "node-sass": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz", @@ -6703,9 +7112,9 @@ "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=" }, "nwsapi": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.4.tgz", - "integrity": "sha512-Zt6HRR6RcJkuj5/N9zeE7FN6YitRW//hK2wTOwX274IBphbY3Zf5+yn5mZ9v/SzAOTMjQNxZf9KkmPLWn0cV4g==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", + "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==", "dev": true }, "oauth-sign": { @@ -6767,6 +7176,27 @@ "es-abstract": "^1.5.1" } }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -6925,6 +7355,35 @@ "pbkdf2": "^3.0.3" } }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -9032,10 +9491,16 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, "pretty-format": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.2.0.tgz", - "integrity": "sha1-OwqqY8AYpTWDNzwcs6XZbMXoMBc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "^3.0.0", @@ -9080,16 +9545,6 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, - "prompts": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.10.tgz", - "integrity": "sha512-/MPwms6+g/m6fvXZlQyOL4m4ziDim2+Wc6CdWVjp+nVCkzEkK2N4rR74m/bbGf+dkta+/SBpo1FfES8Wgrk/Fw==", - "dev": true, - "requires": { - "clorox": "^1.0.3", - "sisteransi": "^0.1.1" - } - }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -9103,9 +9558,9 @@ "dev": true }, "psl": { - "version": "1.1.28", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.28.tgz", - "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw==", + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", "dev": true }, "public-encrypt": { @@ -9182,6 +9637,31 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "randombytes": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", @@ -9267,9 +9747,9 @@ } }, "realpath-native": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", - "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", + "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", "dev": true, "requires": { "util.promisify": "^1.0.0" @@ -9321,6 +9801,11 @@ } } }, + "reduce-reducers": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.4.3.tgz", + "integrity": "sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==" + }, "redux": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz", @@ -9353,6 +9838,15 @@ "private": "^0.1.6" } }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -9577,16 +10071,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -10342,9 +10826,9 @@ } }, "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, "static-extend": { @@ -10428,33 +10912,6 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -10604,18 +11061,62 @@ } }, "test-exclude": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", - "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", + "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", "dev": true, "requires": { "arrify": "^1.0.1", - "micromatch": "^3.1.8", + "micromatch": "^2.3.11", "object-assign": "^4.1.0", "read-pkg-up": "^1.0.1", "require-main-filename": "^1.0.1" }, "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -10626,6 +11127,21 @@ "pinkie-promise": "^2.0.0" } }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -10639,6 +11155,27 @@ "strip-bom": "^2.0.0" } }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", @@ -10798,21 +11335,13 @@ } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, "tr46": { @@ -10904,46 +11433,16 @@ "dev": true }, "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", "dev": true, "optional": true, "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "optional": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } + "commander": "~2.17.1", + "source-map": "~0.6.1" } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, "uglifyjs-webpack-plugin": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", @@ -11433,26 +11932,29 @@ } }, "whatwg-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", - "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", "dev": true, "requires": { - "iconv-lite": "0.4.19" + "iconv-lite": "0.4.24" }, "dependencies": { "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } } } }, "whatwg-mimetype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", - "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", "dev": true }, "whatwg-url": { @@ -11496,13 +11998,6 @@ "string-width": "^1.0.2 || 2" } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -11577,13 +12072,12 @@ } }, "ws": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", - "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", "dev": true, "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0" + "async-limiter": "~1.0.0" } }, "xml-name-validator": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index be66f7508884..aa13b6c4a481 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -20,6 +20,8 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.1.0-4", + "highlight.js": "^9.13.1", + "highlightjs-solidity": "^1.0.6", "bignumber.js": "^7.2.1", "bootstrap": "^4.1.3", "chart.js": "^2.7.2", @@ -34,6 +36,7 @@ "phoenix": "file:../../../deps/phoenix", "phoenix_html": "file:../../../deps/phoenix_html", "popper.js": "^1.14.3", + "reduce-reducers": "^0.4.3", "redux": "^4.0.0", "urijs": "^1.19.1" }, diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 3f3eecb4e167..38d5b3cd1f16 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -18,7 +18,7 @@ config :block_scout_web, BlockScoutWeb.Chain, # Configures the endpoint config :block_scout_web, BlockScoutWeb.Endpoint, - instrumenters: [BlockScoutWeb.Prometheus.Instrumenter], + instrumenters: [BlockScoutWeb.Prometheus.Instrumenter, SpandexPhoenix.Instrumenter], url: [ host: "localhost", path: System.get_env("NETWORK_PATH") || "/" @@ -26,6 +26,11 @@ config :block_scout_web, BlockScoutWeb.Endpoint, render_errors: [view: BlockScoutWeb.ErrorView, accepts: ~w(html json)], pubsub: [name: BlockScoutWeb.PubSub, adapter: Phoenix.PubSub.PG2] +config :block_scout_web, BlockScoutWeb.Tracer, + service: :block_scout_web, + adapter: SpandexDatadog.Adapter, + trace_key: :blockscout + # Configures gettext config :block_scout_web, BlockScoutWeb.Gettext, locales: ~w(en), default_locale: "en" @@ -42,10 +47,14 @@ config :ex_cldr, config :logger, :block_scout_web, # keep synced with `config/config.exs` - format: "$time $metadata[$level] $message\n", - metadata: [:application, :request_id], + format: "$dateT$time $metadata[$level] $message\n", + metadata: + ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count + block_number step count error_count shrunk)a, metadata_filter: [application: :block_scout_web] +config :spandex_phoenix, tracer: BlockScoutWeb.Tracer + config :wobserver, # return only the local node discovery: :none, diff --git a/apps/block_scout_web/config/dev.exs b/apps/block_scout_web/config/dev.exs index d821ac555f20..2b5018366761 100644 --- a/apps/block_scout_web/config/dev.exs +++ b/apps/block_scout_web/config/dev.exs @@ -48,6 +48,8 @@ config :block_scout_web, BlockScoutWeb.Endpoint, ] ] +config :block_scout_web, BlockScoutWeb.Tracer, env: "dev", disabled?: true + config :logger, :block_scout_web, level: :debug, path: Path.absname("logs/dev/block_scout_web.log") diff --git a/apps/block_scout_web/config/prod.exs b/apps/block_scout_web/config/prod.exs index a6de66fea40d..1fb27bdbed80 100644 --- a/apps/block_scout_web/config/prod.exs +++ b/apps/block_scout_web/config/prod.exs @@ -24,6 +24,8 @@ config :block_scout_web, BlockScoutWeb.Endpoint, port: System.get_env("PORT") ] +config :block_scout_web, BlockScoutWeb.Tracer, env: "production", disabled?: true + config :logger, :block_scout_web, level: :info, path: Path.absname("logs/prod/block_scout_web.log"), diff --git a/apps/block_scout_web/config/test.exs b/apps/block_scout_web/config/test.exs index 74c119ab45e1..e77d3db10dcc 100644 --- a/apps/block_scout_web/config/test.exs +++ b/apps/block_scout_web/config/test.exs @@ -9,9 +9,13 @@ config :block_scout_web, BlockScoutWeb.Endpoint, secret_key_base: "27Swe6KtEtmN37WyEYRjKWyxYULNtrxlkCEKur4qoV+Lwtk8lafsR16ifz1XBBYj", server: true +config :block_scout_web, BlockScoutWeb.Tracer, disabled?: false + config :logger, :block_scout_web, level: :warn, path: Path.absname("logs/test/block_scout_web.log") # Configure wallaby config :wallaby, screenshot_on_failure: true + +config :explorer, Explorer.ExchangeRates, enabled: false, store: :none 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 1d956cfa65c8..c132d72307dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.{ Address, + Address.CoinBalance, Address.CurrentTokenBalance, Block, InternalTransaction, @@ -194,6 +195,10 @@ defmodule BlockScoutWeb.Chain do %{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)} end + defp paging_params(%CoinBalance{block_number: block_number}) do + %{"block_number" => block_number} + end + defp block_or_transaction_from_param(param) do with {:error, :not_found} <- transaction_from_param(param) do hash_string_to_block(param) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index 6213b6aaf59c..83d6d47a3427 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -4,11 +4,11 @@ defmodule BlockScoutWeb.AddressChannel do """ use BlockScoutWeb, :channel - alias BlockScoutWeb.{AddressView, InternalTransactionView, TransactionView} + alias BlockScoutWeb.{AddressCoinBalanceView, AddressView, InternalTransactionView, TransactionView} alias Explorer.Chain.Hash alias Phoenix.View - intercept(["balance_update", "count", "internal_transaction", "pending_transaction", "transaction"]) + intercept(["balance_update", "coin_balance", "count", "internal_transaction", "transaction"]) def join("addresses:" <> _address_hash, _params, socket) do {:ok, %{}, socket} @@ -62,7 +62,24 @@ defmodule BlockScoutWeb.AddressChannel do end def handle_out("transaction", data, socket), do: handle_transaction(data, socket, "transaction") - def handle_out("pending_transaction", data, socket), do: handle_transaction(data, socket, "pending_transaction") + + def handle_out("coin_balance", %{coin_balance: coin_balance}, socket) do + Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) + + rendered_coin_balance = + View.render_to_string( + AddressCoinBalanceView, + "_coin_balances.html", + conn: socket, + coin_balance: coin_balance + ) + + push(socket, "coin_balance", %{ + coin_balance_html: rendered_coin_balance + }) + + {:noreply, socket} + end def handle_transaction(%{address: address, transaction: transaction}, socket, event) do Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_by_day_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_by_day_controller.ex new file mode 100644 index 000000000000..6af67c4861c5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_by_day_controller.ex @@ -0,0 +1,17 @@ +defmodule BlockScoutWeb.AddressCoinBalanceByDayController do + @moduledoc """ + Manages the grouping by day of the coin balance history of an address + """ + + use BlockScoutWeb, :controller + + alias Explorer.Chain + + def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"}) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do + balances_by_day = Chain.address_to_balances_by_day(address_hash) + + json(conn, balances_by_day) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex new file mode 100644 index 000000000000..3346b70f5e85 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex @@ -0,0 +1,71 @@ +defmodule BlockScoutWeb.AddressCoinBalanceController do + @moduledoc """ + Manages the displaying of information about the coin balance history of an address + """ + + use BlockScoutWeb, :controller + + import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + alias BlockScoutWeb.AddressCoinBalanceView + alias Explorer.{Chain, Market} + alias Explorer.ExchangeRates.Token + alias Phoenix.View + + 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 + full_options = paging_options(params) + + coin_balances_plus_one = Chain.address_to_coin_balances(address_hash, full_options) + + {coin_balances, next_page} = split_list_by_page(coin_balances_plus_one) + + next_page_url = + case next_page_params(next_page, coin_balances, params) do + nil -> + nil + + next_page_params -> + address_coin_balance_path( + conn, + :index, + address, + Map.delete(next_page_params, "type") + ) + end + + coin_balances_json = + Enum.map(coin_balances, fn coin_balance -> + View.render_to_string( + AddressCoinBalanceView, + "_coin_balances.html", + conn: conn, + coin_balance: coin_balance + ) + end) + + json(conn, %{items: coin_balances_json, next_page_path: next_page_url}) + else + :error -> + unprocessable_entity(conn) + + {:error, :not_found} -> + not_found(conn) + end + end + + def index(conn, %{"address_id" => address_hash_string}) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash) do + render(conn, "index.html", + address: address, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + transaction_count: transaction_count(address), + validation_count: validation_count(address), + current_path: current_path(conn) + ) + end + end +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 22825ae63ae2..a0c443aee675 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 @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.AddressController do def index(conn, _params) do render(conn, "index.html", address_tx_count_pairs: Chain.list_top_addresses(), - address_estimated_count: Chain.address_estimated_count(), + address_count: Chain.count_addresses_with_balance_from_cache(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), total_supply: Chain.total_supply() ) 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 9160d09db540..a195f271d603 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 @@ -10,7 +10,6 @@ defmodule BlockScoutWeb.AddressTransactionController do alias BlockScoutWeb.TransactionView alias Explorer.{Chain, Market} - alias Explorer.Chain.Hash alias Explorer.ExchangeRates.Token alias Phoenix.View @@ -48,29 +47,21 @@ defmodule BlockScoutWeb.AddressTransactionController do conn, :index, address, - next_page_params + Map.delete(next_page_params, "type") ) 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 - } - ) + transactions_json = + Enum.map(transactions, fn transaction -> + View.render_to_string( + TransactionView, + "_tile.html", + current_address: address, + transaction: transaction + ) + end) + + json(conn, %{items: transactions_json, next_page_path: next_page_url}) else :error -> unprocessable_entity(conn) @@ -90,7 +81,8 @@ defmodule BlockScoutWeb.AddressTransactionController do exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], transaction_count: transaction_count(address), - validation_count: validation_count(address) + validation_count: validation_count(address), + current_path: current_path(conn) ) else :error -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex index 6d39df12b5cb..90944822abae 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex @@ -5,12 +5,16 @@ defmodule BlockScoutWeb.AddressValidationController do use BlockScoutWeb, :controller import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] - import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] - alias Explorer.{Chain, Market} + import BlockScoutWeb.Chain, + only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + alias BlockScoutWeb.BlockView alias Explorer.ExchangeRates.Token + alias Explorer.{Chain, Market} + alias Phoenix.View - def index(conn, %{"address_id" => address_hash_string} = params) do + 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.find_or_insert_address_from_hash(address_hash) do full_options = @@ -28,15 +32,52 @@ defmodule BlockScoutWeb.AddressValidationController do blocks_plus_one = Chain.get_blocks_validated_by_address(full_options, address) {blocks, next_page} = split_list_by_page(blocks_plus_one) + next_page_path = + case next_page_params(next_page, blocks, params) do + nil -> + nil + + next_page_params -> + address_validation_path( + conn, + :index, + address_hash_string, + Map.delete(next_page_params, "type") + ) + end + + items = + Enum.map(blocks, fn block -> + View.render_to_string( + BlockView, + "_tile.html", + conn: conn, + block: block, + block_type: BlockView.block_type(block) + ) + end) + + json(conn, %{items: items, next_page_path: next_page_path}) + else + :error -> + unprocessable_entity(conn) + + {:error, :not_found} -> + not_found(conn) + end + end + + def index(conn, %{"address_id" => address_hash_string}) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash) do render( conn, "index.html", address: address, - blocks: blocks, + current_path: current_path(conn), transaction_count: transaction_count(address), validation_count: validation_count(address), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), - next_page_params: next_page_params(next_page, blocks, params) + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() ) else :error -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex index 65a90460cbee..c1246bcb7f30 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex @@ -52,7 +52,7 @@ defmodule BlockScoutWeb.BlockController do {blocks, next_page} = split_list_by_page(blocks_plus_one) - next_page_url = + next_page_path = case next_page_params(next_page, blocks, params) do nil -> nil @@ -61,7 +61,7 @@ defmodule BlockScoutWeb.BlockController do block_path( conn, :index, - next_page_params + Map.delete(next_page_params, "type") ) end @@ -70,35 +70,23 @@ defmodule BlockScoutWeb.BlockController do json( conn, %{ - blocks: + items: Enum.map(blocks, fn block -> - %{ - block_number: block.number, - block_html: - View.render_to_string( - BlockView, - "_tile.html", - block: block, - block_type: block_type - ) - } + View.render_to_string( + BlockView, + "_tile.html", + block: block, + block_type: block_type + ) end), - next_page_url: next_page_url + next_page_path: next_page_path } ) end - defp handle_render(full_options, conn, params) do - blocks_plus_one = - full_options - |> Keyword.merge(paging_options(%{})) - |> Chain.list_blocks() - - {blocks, next_page} = split_list_by_page(blocks_plus_one) - + defp handle_render(full_options, conn, _params) do render(conn, "index.html", - blocks: blocks, - next_page_params: next_page_params(next_page, blocks, params), + current_path: current_path(conn), block_type: Keyword.get(full_options, :block_type, "Block") ) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 73bd85915e43..1929bb352eda 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -36,7 +36,7 @@ defmodule BlockScoutWeb.ChainController do render( conn, "show.html", - address_estimated_count: Chain.address_estimated_count(), + address_count: Chain.count_addresses_with_balance_from_cache(), average_block_time: Chain.average_block_time(), blocks: blocks, exchange_rate: exchange_rate, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex index ab37215c09a4..95ffe8aa2275 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.PendingTransactionController do alias BlockScoutWeb.TransactionView alias Explorer.Chain - alias Explorer.Chain.Hash alias Phoenix.View def index(conn, %{"type" => "JSON"} = params) do @@ -31,52 +30,30 @@ defmodule BlockScoutWeb.PendingTransactionController do pending_transaction_path( conn, :index, - next_page_params + Map.delete(next_page_params, "type") ) end json( conn, %{ - pending_transactions: + items: Enum.map(transactions, fn transaction -> - %{ - transaction_hash: Hash.to_string(transaction.hash), - transaction_html: - View.render_to_string( - TransactionView, - "_tile.html", - transaction: transaction - ) - } + View.render_to_string( + TransactionView, + "_tile.html", + transaction: transaction + ) end), - next_page_url: next_page_url + next_page_path: next_page_url } ) end - def index(conn, params) do - full_options = - Keyword.merge( - [ - necessity_by_association: %{ - [from_address: :names] => :optional, - [to_address: :names] => :optional - } - ], - paging_options(%{}) - ) - - {transactions, next_page} = get_pending_transactions_and_next_page(full_options) - - pending_transaction_count = Chain.pending_transaction_count() - - render( - conn, - "index.html", - next_page_params: next_page_params(next_page, transactions, params), - pending_transaction_count: pending_transaction_count, - transactions: transactions + def index(conn, _params) do + render(conn, "index.html", + current_path: current_path(conn), + pending_transaction_count: Chain.pending_transaction_count() ) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex index d36eec0a3e7a..b66aa09bb9ae 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex @@ -1,7 +1,9 @@ defmodule BlockScoutWeb.Tokens.HolderController do use BlockScoutWeb, :controller + alias BlockScoutWeb.Tokens.HolderView alias Explorer.Chain + alias Phoenix.View import BlockScoutWeb.Chain, only: [ @@ -10,21 +12,47 @@ defmodule BlockScoutWeb.Tokens.HolderController do next_page_params: 3 ] - def index(conn, %{"token_id" => address_hash_string} = params) do + def index(conn, %{"token_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, token} <- Chain.token_from_address_hash(address_hash), token_balances <- Chain.fetch_token_holders_from_token_hash(address_hash, paging_options(params)) do {token_balances_paginated, next_page} = split_list_by_page(token_balances) + next_page_path = + case next_page_params(next_page, token_balances_paginated, params) do + nil -> + nil + + next_page_params -> + token_holder_path(conn, :index, address_hash, Map.delete(next_page_params, "type")) + end + + token_balances_json = + Enum.map(token_balances_paginated, fn token_balance -> + View.render_to_string(HolderView, "_token_balances.html", token_balance: token_balance, token: token) + end) + + json(conn, %{items: token_balances_json, next_page_path: next_page_path}) + else + :error -> + not_found(conn) + + {:error, :not_found} -> + not_found(conn) + end + end + + def index(conn, %{"token_id" => address_hash_string}) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, token} <- Chain.token_from_address_hash(address_hash) do render( conn, "index.html", - token: token, - token_balances: token_balances_paginated, + current_path: current_path(conn), holders_count_consolidation_enabled: Chain.token_holders_counter_consolidation_enabled?(), - total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash), + token: token, total_token_holders: Chain.count_token_holders_from_token_hash(address_hash), - next_page_params: next_page_params(next_page, token_balances_paginated, params) + total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash) ) else :error -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index 8ad991cef69e..b40e6988e0bd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.TransactionController do alias BlockScoutWeb.TransactionView alias Explorer.Chain - alias Explorer.Chain.Hash alias Phoenix.View def index(conn, %{"type" => "JSON"} = params) do @@ -22,65 +21,42 @@ defmodule BlockScoutWeb.TransactionController do paging_options(params) ) - {transactions, next_page} = get_transactions_and_next_page(full_options) + transactions_plus_one = Chain.recent_collated_transactions(full_options) + {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_url = + next_page_path = case next_page_params(next_page, transactions, params) do nil -> nil next_page_params -> - transaction_path( - conn, - :index, - next_page_params - ) + transaction_path(conn, :index, Map.delete(next_page_params, "type")) end json( conn, %{ - transactions: + items: Enum.map(transactions, fn transaction -> - %{ - transaction_hash: Hash.to_string(transaction.hash), - transaction_html: - View.render_to_string( - TransactionView, - "_tile.html", - transaction: transaction - ) - } + View.render_to_string( + TransactionView, + "_tile.html", + transaction: transaction + ) end), - next_page_url: next_page_url + next_page_path: next_page_path } ) end - def index(conn, params) do - full_options = - Keyword.merge( - [ - necessity_by_association: %{ - :block => :required, - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional - } - ], - paging_options(%{}) - ) - - {transactions, next_page} = get_transactions_and_next_page(full_options) - + def index(conn, _params) do transaction_estimated_count = Chain.transaction_estimated_count() render( conn, "index.html", - next_page_params: next_page_params(next_page, transactions, params), - transaction_estimated_count: transaction_estimated_count, - transactions: transactions + current_path: current_path(conn), + transaction_estimated_count: transaction_estimated_count ) end @@ -102,9 +78,4 @@ defmodule BlockScoutWeb.TransactionController do redirect(conn, to: transaction_internal_transaction_path(conn, :index, id)) end end - - defp get_transactions_and_next_page(options) do - transactions_plus_one = Chain.recent_collated_transactions(options) - split_list_by_page(transactions_plus_one) - end end diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index 673f3ea7c6c8..9066000a4915 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -68,8 +68,13 @@ defmodule BlockScoutWeb.Endpoint do signing_salt: "iC2ksJHS" ) + use SpandexPhoenix + plug(BlockScoutWeb.Prometheus.Exporter) + # 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint + plug(CORSPlug, headers: ["x-apollo-tracing" | CORSPlug.defaults()[:headers]]) + plug(BlockScoutWeb.Router) def init(_key, config) do 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 a9890e42936b..e8b14869cc5f 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -1012,7 +1012,7 @@ defmodule BlockScoutWeb.Etherscan do @account_txlistinternal_action %{ name: "txlistinternal", description: - "Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions.", + "Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions. Also available through a GraphQL 'transaction' query.", required_params: [ %{ key: "txhash", @@ -1087,7 +1087,8 @@ defmodule BlockScoutWeb.Etherscan do @account_tokentx_action %{ name: "tokentx", - description: "Get token transfer events by address. Up to a maximum of 10,000 token transfer events.", + description: + "Get token transfer events by address. Up to a maximum of 10,000 token transfer events. Also available through a GraphQL 'token_transfers' query.", required_params: [ %{ key: "address", diff --git a/apps/block_scout_web/lib/block_scout_web/event_handler.ex b/apps/block_scout_web/lib/block_scout_web/event_handler.ex index d05a18933f77..6b327302bf46 100644 --- a/apps/block_scout_web/lib/block_scout_web/event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/event_handler.ex @@ -18,6 +18,7 @@ defmodule BlockScoutWeb.EventHandler do def init([]) do Chain.subscribe_to_events(:addresses) + Chain.subscribe_to_events(:address_coin_balances) Chain.subscribe_to_events(:blocks) Chain.subscribe_to_events(:exchange_rate) Chain.subscribe_to_events(:internal_transactions) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 6db93c0fce44..7fdacedfec7f 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -10,13 +10,17 @@ defmodule BlockScoutWeb.Notifier do alias Explorer.ExchangeRates.Token def handle_event({:chain_event, :addresses, :realtime, addresses}) do - Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.address_estimated_count()}) + Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_with_balance_from_cache()}) addresses |> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end) |> Enum.each(&broadcast_balance/1) end + def handle_event({:chain_event, :address_coin_balances, :realtime, address_coin_balances}) do + Enum.each(address_coin_balances, &broadcast_address_coin_balance/1) + end + def handle_event({:chain_event, :blocks, :catchup, _blocks}) do ratio = Chain.indexed_ratio() @@ -90,6 +94,14 @@ defmodule BlockScoutWeb.Notifier do def handle_event(_), do: nil + defp broadcast_address_coin_balance(%{address_hash: address_hash, block_number: block_number}) do + coin_balance = Chain.get_coin_balance(address_hash, block_number) + + Endpoint.broadcast("addresses:#{address_hash}", "coin_balance", %{ + coin_balance: coin_balance + }) + end + defp broadcast_balance(%Address{hash: address_hash} = address) do Endpoint.broadcast("addresses:#{address_hash}", "balance_update", %{ address: address, diff --git a/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex b/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex new file mode 100644 index 000000000000..ee20311efc6d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/plug/allow_iframe.ex @@ -0,0 +1,14 @@ +defmodule BlockScoutWeb.Plug.AllowIframe do + @moduledoc """ + Allows for iframes by deleting the + [`X-Frame-Options` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) + """ + + alias Plug.Conn + + def init(opts), do: opts + + def call(conn, _opts) do + Conn.delete_resp_header(conn, "x-frame-options") + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/internal_transaction.ex new file mode 100644 index 000000000000..08f3ca45ce67 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/internal_transaction.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.Resolvers.InternalTransaction do + @moduledoc false + + alias Absinthe.Relay.Connection + alias Explorer.Chain.Transaction + alias Explorer.{GraphQL, Repo} + + def get_by(%{transaction_hash: _, index: _} = args) do + GraphQL.get_internal_transaction(args) + end + + def get_by(%Transaction{} = transaction, args, _) do + transaction + |> GraphQL.transaction_to_internal_transactions_query() + |> Connection.from_query(&Repo.all/1, args, options(args)) + end + + defp options(%{before: _}), do: [] + + defp options(%{count: count}), do: [count: count] + + defp options(_), do: [] +end diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex new file mode 100644 index 000000000000..38b8b3f4acaf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/token_transfer.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Resolvers.TokenTransfer do + @moduledoc false + + alias Absinthe.Relay.Connection + alias Explorer.{GraphQL, Repo} + + def get_by(%{transaction_hash: _, log_index: _} = args) do + GraphQL.get_token_transfer(args) + end + + def get_by(_, %{token_contract_address_hash: token_contract_address_hash} = args, _) do + connection_args = Map.take(args, [:after, :before, :first, :last]) + + token_contract_address_hash + |> GraphQL.list_token_transfers_query() + |> Connection.from_query(&Repo.all/1, connection_args, options(args)) + end + + defp options(%{before: _}), do: [] + + defp options(%{count: count}), do: [count: count] + + defp options(_), do: [] +end diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex index c13aa2a561a1..aa54a8e6dae0 100644 --- a/apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.Resolvers.Transaction do def get_by(_, %{hash: hash}, _) do case Chain.hash_to_transaction(hash) do {:ok, transaction} -> {:ok, transaction} - {:error, :not_found} -> {:error, "Transaction hash #{hash} was not found."} + {:error, :not_found} -> {:error, "Transaction not found."} 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 577a414163a5..c17327f0c55d 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -55,8 +55,14 @@ defmodule BlockScoutWeb.Router do max_complexity: @max_complexity ) + # Disallows Iframes (write routes) scope "/", BlockScoutWeb do pipe_through(:browser) + end + + # Allows Iframes (read-only routes) + scope "/", BlockScoutWeb do + pipe_through([:browser, BlockScoutWeb.Plug.AllowIframe]) resources("/", ChainController, only: [:show], singleton: true, as: :chain) @@ -143,6 +149,20 @@ defmodule BlockScoutWeb.Router do only: [:index], as: :token_balance ) + + resources( + "/coin_balances", + AddressCoinBalanceController, + only: [:index], + as: :coin_balance + ) + + resources( + "/coin_balances/by_day", + AddressCoinBalanceByDayController, + only: [:index], + as: :coin_balance_by_day + ) end resources "/tokens", Tokens.TokenController, only: [:show], as: :token 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 c310ea844ac1..42d644c41830 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema.ex @@ -6,14 +6,30 @@ defmodule BlockScoutWeb.Schema do alias Absinthe.Middleware.Dataloader, as: AbsintheMiddlewareDataloader alias Absinthe.Plugin, as: AbsinthePlugin - alias BlockScoutWeb.Resolvers.{Address, Block, Transaction} + + alias BlockScoutWeb.Resolvers.{ + Address, + Block, + InternalTransaction, + TokenTransfer, + Transaction + } + alias Explorer.Chain + alias Explorer.Chain.InternalTransaction, as: ExplorerChainInternalTransaction + alias Explorer.Chain.TokenTransfer, as: ExplorerChainTokenTransfer alias Explorer.Chain.Transaction, as: ExplorerChainTransaction import_types(BlockScoutWeb.Schema.Types) node interface do resolve_type(fn + %ExplorerChainInternalTransaction{}, _ -> + :internal_transaction + + %ExplorerChainTokenTransfer{}, _ -> + :token_transfer + %ExplorerChainTransaction{}, _ -> :transaction @@ -25,6 +41,16 @@ defmodule BlockScoutWeb.Schema do query do node field do resolve(fn + %{type: :internal_transaction, id: id}, _ -> + %{"transaction_hash" => transaction_hash_string, "index" => index} = Jason.decode!(id) + {:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string) + InternalTransaction.get_by(%{transaction_hash: transaction_hash, index: index}) + + %{type: :token_transfer, id: id}, _ -> + %{"transaction_hash" => transaction_hash_string, "log_index" => log_index} = Jason.decode!(id) + {:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string) + TokenTransfer.get_by(%{transaction_hash: transaction_hash, log_index: log_index}) + %{type: :transaction, id: transaction_hash_string}, _ -> {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) Transaction.get_by(%{}, %{hash: hash}, %{}) @@ -53,6 +79,22 @@ defmodule BlockScoutWeb.Schema do resolve(&Block.get_by/3) end + @desc "Gets token transfers by token contract address hash." + connection field(:token_transfers, node_type: :token_transfer) do + arg(:token_contract_address_hash, non_null(:address_hash)) + arg(:count, :integer) + + resolve(&TokenTransfer.get_by/3) + + complexity(fn + %{first: first}, child_complexity -> + first * child_complexity + + %{last: last}, child_complexity -> + last * child_complexity + end) + end + @desc "Gets a transaction by hash." field :transaction, :transaction do arg(:hash, non_null(:full_hash)) 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 675992eea4ac..b7a8939a3986 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 @@ -99,4 +99,18 @@ defmodule BlockScoutWeb.Schema.Scalars do value(:ok) value(:error) end + + enum :call_type do + value(:call) + value(:callcode) + value(:delegatecall) + value(:staticcall) + end + + enum :type do + value(:call) + value(:create) + value(:reward) + value(:selfdestruct) + end end 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 3492c71fe986..029b322f9c46 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,17 @@ defmodule BlockScoutWeb.Schema.Types do import Absinthe.Resolution.Helpers - alias BlockScoutWeb.Resolvers.Transaction + alias BlockScoutWeb.Resolvers.{ + InternalTransaction, + Transaction + } import_types(Absinthe.Type.Custom) import_types(BlockScoutWeb.Schema.Scalars) connection(node_type: :transaction) + connection(node_type: :internal_transaction) + connection(node_type: :token_transfer) @desc """ A stored representation of a Web3 address. @@ -60,6 +65,30 @@ defmodule BlockScoutWeb.Schema.Types do field(:parent_hash, :full_hash) end + @desc """ + Models internal transactions. + """ + node object(:internal_transaction, id_fetcher: &internal_transaction_id_fetcher/2) do + field(:call_type, :call_type) + field(:created_contract_code, :data) + field(:error, :string) + field(:gas, :decimal) + field(:gas_used, :decimal) + field(:index, :integer) + field(:init, :data) + field(:input, :data) + field(:output, :data) + field(:trace_address, :json) + field(:type, :type) + field(:value, :wei) + field(:block_number, :integer) + field(:transaction_index, :integer) + field(:created_contract_address_hash, :address_hash) + field(:from_address_hash, :address_hash) + field(:to_address_hash, :address_hash) + field(:transaction_hash, :full_hash) + end + @desc """ The representation of a verified Smart Contract. @@ -77,6 +106,20 @@ defmodule BlockScoutWeb.Schema.Types do field(:address_hash, :address_hash) end + @desc """ + Represents a token transfer between addresses. + """ + node object(:token_transfer, id_fetcher: &token_transfer_id_fetcher/2) do + field(:amount, :decimal) + field(:block_number, :integer) + field(:log_index, :integer) + field(:token_id, :decimal) + field(:from_address_hash, :address_hash) + field(:to_address_hash, :address_hash) + field(:token_contract_address_hash, :address_hash) + field(:transaction_hash, :full_hash) + end + @desc """ Models a Web3 transaction. """ @@ -99,18 +142,28 @@ defmodule BlockScoutWeb.Schema.Types do field(:from_address_hash, :address_hash) field(:to_address_hash, :address_hash) field(:created_contract_address_hash, :address_hash) + + connection field(:internal_transactions, node_type: :internal_transaction) do + arg(:count, :integer) + resolve(&InternalTransaction.get_by/3) + + complexity(fn + %{first: first}, child_complexity -> + first * child_complexity + + %{last: last}, child_complexity -> + last * child_complexity + end) + end end - @desc """ - Represents a token transfer between addresses. - """ - object :token_transfer do - field(:amount, :decimal) - field(:from_address_hash, :address_hash) - field(:to_address_hash, :address_hash) - field(:token_contract_address_hash, :address_hash) - field(:transaction_hash, :full_hash) + def token_transfer_id_fetcher(%{transaction_hash: transaction_hash, log_index: log_index}, _) do + Jason.encode!(%{transaction_hash: to_string(transaction_hash), log_index: log_index}) end def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash) + + def internal_transaction_id_fetcher(%{transaction_hash: transaction_hash, index: index}, _) do + Jason.encode!(%{transaction_hash: to_string(transaction_hash), index: index}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_responsive_hash.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_responsive_hash.html.eex index bea2a2159f7b..f88f689a4682 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_responsive_hash.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_responsive_hash.html.eex @@ -1,6 +1,6 @@ <%= if name = primary_name(@address) do %> - <%= name %> (<%= short_hash(@address) %>...) + <%= name %> (<%= short_hash(@address) %>...) <% else %> <%= if assigns[:truncate] do %> <%= BlockScoutWeb.AddressView.trimmed_hash(@address.hash) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex index d9c56c6ecce2..5b99d9c97cc3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex @@ -25,11 +25,20 @@ ) %> + + <%= if BlockScoutWeb.AddressView.validator?(@validation_count) do %>