diff --git a/src/containers/App/App.tsx b/src/containers/App/App.tsx index c2a1ef70e..1cb1fe479 100644 --- a/src/containers/App/App.tsx +++ b/src/containers/App/App.tsx @@ -10,7 +10,7 @@ import { updateViewportDimensions, onScroll } from './actions' import Ledgers from '../Ledgers' import Header from '../Header' import './app.scss' -import ledger from '../Ledger' +import { Ledger } from '../Ledger' import transactions from '../Transactions' import { Network } from '../Network' import { Validator } from '../Validators' @@ -105,7 +105,7 @@ const App = ({ actions }: AppProps) => {
- + { const messages = [ - ['ledgerError', state.ledger.error], ['transactionError', state.transaction.error], ['balanceError', state.accountHeader.error], ['payStringError', state.payStringData.error], diff --git a/src/containers/Header/test/Banner.test.js b/src/containers/Header/test/Banner.test.js index 0d5a29902..0875e7b5a 100644 --- a/src/containers/Header/test/Banner.test.js +++ b/src/containers/Header/test/Banner.test.js @@ -29,16 +29,13 @@ describe('Banner component', () => { it('renders with messages', () => { const state = { ...initialState, - ledger: { - error: 'ledger_error', - }, transaction: { error: 'transaction_error', }, } const wrapper = createWrapper(state) - expect(wrapper.find('.notification').length).toEqual(2) + expect(wrapper.find('.notification').length).toEqual(1) wrapper.unmount() }) }) diff --git a/src/containers/Ledger/actionTypes.js b/src/containers/Ledger/actionTypes.js deleted file mode 100644 index 53d1df2e8..000000000 --- a/src/containers/Ledger/actionTypes.js +++ /dev/null @@ -1,4 +0,0 @@ -export const START_LOADING_FULL_LEDGER = 'START_LOADING_FULL_LEDGER' -export const FINISH_LOADING_FULL_LEDGER = 'FINISH_LOADING_FULL_LEDGER' -export const LOADING_FULL_LEDGER_SUCCESS = 'LOADING_FULL_LEDGER_SUCCESS' -export const LOADING_FULL_LEDGER_FAIL = 'LOADING_FULL_LEDGER_FAIL' diff --git a/src/containers/Ledger/actions.js b/src/containers/Ledger/actions.js deleted file mode 100644 index 4ca20f2b1..000000000 --- a/src/containers/Ledger/actions.js +++ /dev/null @@ -1,45 +0,0 @@ -import { - analytics, - ANALYTIC_TYPES, - BAD_REQUEST, - DECIMAL_REGEX, - HASH_REGEX, -} from '../shared/utils' -import * as actionTypes from './actionTypes' -import { getLedger } from '../../rippled' - -export const loadLedger = (identifier, rippledSocket) => (dispatch) => { - if (!DECIMAL_REGEX.test(identifier) && !HASH_REGEX.test(identifier)) { - dispatch({ - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: { error: BAD_REQUEST }, - }) - return undefined - } - - dispatch({ - type: actionTypes.START_LOADING_FULL_LEDGER, - data: { id: identifier }, - }) - - return getLedger(identifier, rippledSocket) - .then((data) => { - dispatch({ type: actionTypes.FINISH_LOADING_FULL_LEDGER }) - dispatch({ type: actionTypes.LOADING_FULL_LEDGER_SUCCESS, data }) - }) - .catch((error) => { - const status = error.code - analytics(ANALYTIC_TYPES.exception, { - exDescription: `ledger ${identifier} --- ${JSON.stringify(error)}`, - }) - dispatch({ type: actionTypes.FINISH_LOADING_FULL_LEDGER }) - - dispatch({ - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: { error: status, id: identifier }, - error: status === 500 ? 'get_ledger_failed' : '', - }) - }) -} - -export { loadLedger as default } diff --git a/src/containers/Ledger/index.tsx b/src/containers/Ledger/index.tsx index 30f747e81..cebab7af9 100644 --- a/src/containers/Ledger/index.tsx +++ b/src/containers/Ledger/index.tsx @@ -1,9 +1,8 @@ import { useContext, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { Link } from 'react-router-dom' import { useParams } from 'react-router' +import { Link } from 'react-router-dom' +import { useQuery } from 'react-query' import NoMatch from '../NoMatch' import Loader from '../shared/components/Loader' import SocketContext from '../shared/SocketContext' @@ -16,14 +15,16 @@ import { BAD_REQUEST, analytics, ANALYTIC_TYPES, + DECIMAL_REGEX, + HASH_REGEX, } from '../shared/utils' import LeftArrow from '../shared/images/ic_left_arrow.svg' import RightArrow from '../shared/images/ic_right_arrow.svg' -import { loadLedger } from './actions' import { LedgerTransactionTable } from './LedgerTransactionTable' import './ledger.scss' +import { getLedger } from '../../rippled' const TIME_ZONE = 'UTC' const DATE_OPTIONS = { @@ -54,15 +55,7 @@ ERROR_MESSAGES.default = { const getErrorMessage = (error) => ERROR_MESSAGES[error] || ERROR_MESSAGES.default -export interface LedgerProps { - actions: { - loadLedger: Function - } - data: any - loading: boolean -} - -const Ledger = ({ actions, data, loading }: LedgerProps) => { +export const Ledger = () => { const rippledSocket = useContext(SocketContext) const { identifier = '' } = useParams<{ identifier: string }>() const { t } = useTranslation() @@ -74,10 +67,29 @@ const Ledger = ({ actions, data, loading }: LedgerProps) => { title: 'Ledger', path: '/ledgers/:id', }) - actions.loadLedger(identifier, rippledSocket) - }, [actions, identifier, rippledSocket, t]) + }, [identifier, t]) + + const { + data: ledgerData, + error, + isLoading, + } = useQuery(['ledger', identifier], () => { + if (!DECIMAL_REGEX.test(identifier) && !HASH_REGEX.test(identifier)) { + return Promise.reject(BAD_REQUEST) + } + + return getLedger(identifier, rippledSocket).catch( + (transactionRequestError) => { + const status = transactionRequestError.code + analytics(ANALYTIC_TYPES.exception, { + exDescription: `ledger ${identifier} --- ${JSON.stringify(error)}`, + }) + return Promise.reject(status) + }, + ) + }) - const renderNav = () => { + const renderNav = (data: any) => { const { ledger_index: LedgerIndex, ledger_hash: LedgerHash } = data const previousIndex = LedgerIndex - 1 const nextIndex = LedgerIndex + 1 @@ -133,45 +145,30 @@ const Ledger = ({ actions, data, loading }: LedgerProps) => { } const renderLedger = () => - data.ledger_hash ? ( + ledgerData?.ledger_hash ? ( <> - {renderNav()} + {renderNav(ledgerData)} ) : null const renderError = () => { - if (!data.error) { + if (!error) { return null } - const message = getErrorMessage(data.error) + const message = getErrorMessage(error) return } return (
- {loading && } + {isLoading && } {renderLedger()} {renderError()}
) } - -export default connect( - (state: any) => ({ - loading: state.ledger.loading, - data: state.ledger.data, - }), - (dispatch) => ({ - actions: bindActionCreators( - { - loadLedger, - }, - dispatch, - ), - }), -)(Ledger) diff --git a/src/containers/Ledger/reducer.js b/src/containers/Ledger/reducer.js deleted file mode 100644 index 6a79631c6..000000000 --- a/src/containers/Ledger/reducer.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as actionTypes from './actionTypes' - -export const initialState = { - loading: false, - error: '', - data: {}, -} - -const rehydrate = (action) => { - const payload = - action.payload && action.payload.ledger ? action.payload.ledger : {} - const ledgerData = payload.data && !payload.data.error ? payload.data : {} - return { ...payload, loading: false, error: '', data: ledgerData } -} - -const ledgerReducer = (state = initialState, action) => { - switch (action.type) { - case actionTypes.START_LOADING_FULL_LEDGER: - return { ...state, loading: true, data: action.data } - case actionTypes.FINISH_LOADING_FULL_LEDGER: - return { ...state, loading: false } - case actionTypes.LOADING_FULL_LEDGER_SUCCESS: - return { ...state, error: '', data: action.data } - case actionTypes.LOADING_FULL_LEDGER_FAIL: - return { ...state, data: action.data, error: action.error } - case 'persist/REHYDRATE': - return { ...state, ...rehydrate(action) } - default: - return state - } -} - -export default ledgerReducer diff --git a/src/containers/Ledger/test/Ledger.test.js b/src/containers/Ledger/test/Ledger.test.js index 2b63b1c5e..0a60cdbf9 100644 --- a/src/containers/Ledger/test/Ledger.test.js +++ b/src/containers/Ledger/test/Ledger.test.js @@ -1,30 +1,45 @@ import { mount } from 'enzyme' import { I18nextProvider } from 'react-i18next' -import configureMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' -import { Provider } from 'react-redux' -import { BrowserRouter as Router } from 'react-router-dom' +import { MemoryRouter as Router, Route } from 'react-router-dom' +import { QueryClientProvider } from 'react-query' import mockLedger from './storedLedger.json' import i18n from '../../../i18n/testConfig' -import { initialState } from '../../../rootReducer' -import { NOT_FOUND, BAD_REQUEST, SERVER_ERROR } from '../../shared/utils' -import Ledger from '../index' +import { Ledger } from '../index' +import { getLedger } from '../../../rippled' +import { testQueryClient } from '../../test/QueryClient' +import { Error as RippledError } from '../../../rippled/lib/utils' + +jest.mock('../../../rippled', () => { + const originalModule = jest.requireActual('../../../rippled') + + return { + __esModule: true, + ...originalModule, + getLedger: jest.fn(), + } +}) + +const mockedGetLedger = getLedger + +export function flushPromises() { + return new Promise((resolve) => setImmediate(resolve)) +} describe('Ledger container', () => { - const middlewares = [thunk] - const mockStore = configureMockStore(middlewares) - const createWrapper = (state = {}) => { - const store = mockStore({ ...initialState, ...state }) - return mount( + const createWrapper = (identifier = 38079857) => + mount( - - - + + + - + , ) - } + + afterEach(() => { + mockedGetLedger.mockReset() + }) it('renders without crashing', () => { const wrapper = createWrapper() @@ -32,20 +47,18 @@ describe('Ledger container', () => { }) it('renders loading', () => { - const state = { ...initialState } - state.ledger.data = {} - state.ledger.loading = true - const wrapper = createWrapper(state) + const wrapper = createWrapper() expect(wrapper.find('.loader').length).toBe(1) wrapper.unmount() }) - it('renders ledger navbar', () => { - const state = { ...initialState } - state.ledger.data = mockLedger - state.ledger.loading = false - state.ledger.error = false - const wrapper = createWrapper(state) + it('renders ledger navbar', async () => { + mockedGetLedger.mockImplementation(() => Promise.resolve(mockLedger)) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() + const header = wrapper.find('.ledger-header') expect(header.length).toBe(1) expect(header.find('.ledger-nav').length).toBe(1) @@ -53,12 +66,13 @@ describe('Ledger container', () => { wrapper.unmount() }) - it('renders ledger summary', () => { - const state = { ...initialState } - state.ledger.data = mockLedger - state.ledger.loading = false - state.ledger.error = false - const wrapper = createWrapper(state) + it('renders ledger summary', async () => { + mockedGetLedger.mockImplementation(() => Promise.resolve(mockLedger)) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() + const summary = wrapper.find('.ledger-header .ledger-info') expect(summary.length).toBe(1) @@ -71,12 +85,13 @@ describe('Ledger container', () => { wrapper.unmount() }) - it('renders transaction table header', () => { - const state = { ...initialState } - state.ledger.data = mockLedger - state.ledger.loading = false - state.ledger.error = false - const wrapper = createWrapper(state) + it('renders transaction table header', async () => { + mockedGetLedger.mockImplementation(() => Promise.resolve(mockLedger)) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() + const table = wrapper.find('.transaction-table') expect(table.length).toBe(1) expect(table.find('.transaction-li-header').length).toBe(1) @@ -85,12 +100,13 @@ describe('Ledger container', () => { wrapper.unmount() }) - it('renders all transactions', () => { - const state = { ...initialState } - state.ledger.data = mockLedger - state.ledger.loading = false - state.ledger.error = false - const wrapper = createWrapper(state) + it('renders all transactions', async () => { + mockedGetLedger.mockImplementation(() => Promise.resolve(mockLedger)) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() + const table = wrapper.find('.transaction-table') expect(table.length).toBe(1) expect(table.find('.transaction-li').length).toBe( @@ -99,34 +115,41 @@ describe('Ledger container', () => { wrapper.unmount() }) - it('renders 404 page on no match', () => { - const state = { ...initialState } - state.ledger.data = { error: NOT_FOUND, id: 1 } - state.ledger.loading = false - state.ledger.error = true + it('renders 404 page on no match', async () => { + mockedGetLedger.mockImplementation(() => + Promise.reject(new RippledError('ledger not found', 404)), + ) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() - const wrapper = createWrapper(state) expect(wrapper.find('.no-match .title').text()).toEqual('ledger_not_found') wrapper.unmount() }) - it('renders server error', () => { - const state = { ...initialState } - state.ledger.data = { error: SERVER_ERROR, id: 1 } - state.ledger.loading = false - state.ledger.error = true + it('renders server error', async () => { + mockedGetLedger.mockImplementation(() => + Promise.reject(new RippledError('ledger failed', 500)), + ) + + const wrapper = createWrapper() + await flushPromises() + wrapper.update() - const wrapper = createWrapper(state) expect(wrapper.find('.no-match .title').text()).toEqual('generic_error') wrapper.unmount() }) - it('renders invalid id error', () => { - const state = { ...initialState } - state.ledger.data = { error: BAD_REQUEST, id: 'zzzz' } - state.ledger.loading = false + it('renders invalid id error', async () => { + mockedGetLedger.mockImplementation(() => + Promise.reject(new RippledError('invalid ledger index/hash', 400)), + ) + + const wrapper = createWrapper('aaaa') + await flushPromises() + wrapper.update() - const wrapper = createWrapper(state) expect(wrapper.find('.no-match .title').text()).toEqual('invalid_ledger_id') wrapper.unmount() }) diff --git a/src/containers/Ledger/test/actions.test.js b/src/containers/Ledger/test/actions.test.js deleted file mode 100644 index 64b88a775..000000000 --- a/src/containers/Ledger/test/actions.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import configureMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' -import mockLedger from './mockLedger.json' -import { NOT_FOUND, BAD_REQUEST, SERVER_ERROR } from '../../shared/utils' -import { initialState } from '../reducer' -import * as actions from '../actions' -import * as actionTypes from '../actionTypes' -import { summarizeLedger } from '../../../rippled/lib/summarizeLedger' -import ledgerNotFound from './ledgerNotFound.json' -import MockWsClient from '../../test/mockWsClient' - -describe('Ledger actions', () => { - const middlewares = [thunk] - const mockStore = configureMockStore(middlewares) - let store - let client - beforeEach(() => { - store = mockStore({ ledger: initialState }) - client = new MockWsClient() - }) - - afterEach(() => { - store = null - client.close() - }) - - it('should dispatch correct actions on success for loadLedger', async () => { - const expectedActions = [ - { - type: actionTypes.START_LOADING_FULL_LEDGER, - data: { id: mockLedger.result.ledger.ledger_index }, - }, - { type: actionTypes.FINISH_LOADING_FULL_LEDGER }, - { - type: actionTypes.LOADING_FULL_LEDGER_SUCCESS, - data: summarizeLedger(mockLedger.result.ledger, true), - }, - ] - client.addResponse('ledger', mockLedger) - - await store.dispatch( - actions.loadLedger(mockLedger.result.ledger.ledger_index, client), - ) - expect(store.getActions()).toEqual(expectedActions) - }) - - it('should dispatch correct actions on success for loadLedger (ledger hash)', async () => { - const expectedActions = [ - { - type: actionTypes.START_LOADING_FULL_LEDGER, - data: { id: mockLedger.result.ledger.ledger_hash }, - }, - { type: actionTypes.FINISH_LOADING_FULL_LEDGER }, - { - type: actionTypes.LOADING_FULL_LEDGER_SUCCESS, - data: summarizeLedger(mockLedger.result.ledger, true), - }, - ] - client.addResponse('ledger', mockLedger) - - await store.dispatch( - actions.loadLedger(mockLedger.result.ledger.ledger_hash, client), - ) - expect(store.getActions()).toEqual(expectedActions) - }) - - it('should dispatch correct actions on fail for loadLedger with invalid id', () => { - const expectedActions = [ - { - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: { error: BAD_REQUEST }, - }, - ] - store.dispatch(actions.loadLedger('zzz', null)) - expect(store.getActions()).toEqual(expectedActions) - }) - - it('should dispatch correct actions on fail for loadLedger 404', async () => { - const LEDGER_INDEX = 1234 - const expectedActions = [ - { - type: actionTypes.START_LOADING_FULL_LEDGER, - data: { id: LEDGER_INDEX }, - }, - { type: actionTypes.FINISH_LOADING_FULL_LEDGER }, - { - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: { - error: NOT_FOUND, - id: LEDGER_INDEX, - }, - error: '', - }, - ] - client.addResponse('ledger', ledgerNotFound) - - await store.dispatch(actions.loadLedger(LEDGER_INDEX, client)) - expect(store.getActions()).toEqual(expectedActions) - }) - - it('should dispatch correct actions on fail for loadLedger 500', async () => { - const expectedActions = [ - { type: actionTypes.START_LOADING_FULL_LEDGER, data: { id: 1 } }, - { type: actionTypes.FINISH_LOADING_FULL_LEDGER }, - { - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - error: 'get_ledger_failed', - data: { - error: SERVER_ERROR, - id: 1, - }, - }, - ] - client.setReturnError() - await store.dispatch(actions.loadLedger(1, client)) - - const receivedActions = store.getActions() - expect(receivedActions).toEqual(expectedActions) - }) -}) diff --git a/src/containers/Ledger/test/reducer.test.js b/src/containers/Ledger/test/reducer.test.js deleted file mode 100644 index dedf65fea..000000000 --- a/src/containers/Ledger/test/reducer.test.js +++ /dev/null @@ -1,85 +0,0 @@ -import * as actionTypes from '../actionTypes' -import reducer, { initialState } from '../reducer' -import mockLedger from './mockLedger.json' - -describe.only('Ledger reducers', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(initialState) - }) - - it('should handle START_LOADING_FULL_LEDGER', () => { - const nextState = { ...initialState, data: { id: 1 }, loading: true } - expect( - reducer(initialState, { - type: actionTypes.START_LOADING_FULL_LEDGER, - data: { id: 1 }, - }), - ).toEqual(nextState) - }) - - it('should handle FINISH_LOADING_FULL_LEDGER', () => { - const nextState = { ...initialState, loading: false } - expect( - reducer(initialState, { type: actionTypes.FINISH_LOADING_FULL_LEDGER }), - ).toEqual(nextState) - }) - - it('should handle LOADING_FULL_LEDGER_SUCCESS', () => { - const nextState = { ...initialState, data: mockLedger } - expect( - reducer(initialState, { - type: actionTypes.LOADING_FULL_LEDGER_SUCCESS, - data: mockLedger, - }), - ).toEqual(nextState) - }) - - it('should handle LOADING_FULL_LEDGER_FAIL', () => { - const error = 'get_ledger_failed' - const nextState = { ...initialState, error } - expect( - reducer(initialState, { - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: {}, - error, - }), - ).toEqual(nextState) - }) - - it('should clear data on rehydration (error)', () => { - const nextState = { - ...initialState, - loading: false, - error: 'get_ledger_failed', - data: { error: 'not found' }, - } - expect( - reducer(initialState, { - type: actionTypes.LOADING_FULL_LEDGER_FAIL, - data: { error: 'not found' }, - error: 'get_ledger_failed', - }), - ).toEqual(nextState) - expect(reducer(nextState, { type: 'persist/REHYDRATE' })).toEqual( - initialState, - ) - }) - - it('should clear data on rehydration (ledger)', () => { - const nextState = { - ...initialState, - loading: false, - error: '', - data: mockLedger, - } - expect( - reducer(initialState, { - type: actionTypes.LOADING_FULL_LEDGER_SUCCESS, - data: mockLedger, - }), - ).toEqual(nextState) - expect(reducer(nextState, { type: 'persist/REHYDRATE' })).toEqual( - initialState, - ) - }) -}) diff --git a/src/rootReducer.js b/src/rootReducer.js index 606f7cff8..0c10781e3 100644 --- a/src/rootReducer.js +++ b/src/rootReducer.js @@ -1,8 +1,5 @@ import { combineReducers } from 'redux' import appReducer, { initialState as appState } from './containers/App/reducer' -import ledgerReducer, { - initialState as ledgerState, -} from './containers/Ledger/reducer' import accountHeaderReducer, { initialState as accountHeaderState, } from './containers/Accounts/AccountHeader/reducer' @@ -19,7 +16,6 @@ import tokenHeaderReducer, { export const initialState = { app: appState, accountHeader: accountHeaderState, - ledger: ledgerState, transaction: transactionState, payStringData: payStringState, tokenHeader: tokenHeaderState, @@ -28,7 +24,6 @@ export const initialState = { const rootReducer = combineReducers({ app: appReducer, accountHeader: accountHeaderReducer, - ledger: ledgerReducer, transaction: transactionReducer, payStringData: payStringReducer, tokenHeader: tokenHeaderReducer,