From 5fd7a5987ba0eec8dee9abde713fb63133b86f7c Mon Sep 17 00:00:00 2001 From: endz80513 <34360753+endz80513@users.noreply.github.com> Date: Sat, 21 Dec 2024 08:39:56 -0800 Subject: [PATCH] Revert "fix: handling of token page throttling (#1100)" This reverts commit 8d503dad9667a77950cdaa8ef929b81e659cc428. --- src/containers/App/index.tsx | 2 +- .../Token/TokenHeader/actionTypes.js | 4 + src/containers/Token/TokenHeader/actions.js | 43 +++++++ src/containers/Token/TokenHeader/index.tsx | 60 ++++++++-- src/containers/Token/TokenHeader/reducer.js | 33 ++++++ .../Token/TokenHeader/test/actions.test.js | 108 ++++++++++++++++++ .../Token/TokenHeader/test/reducer.test.js | 79 +++++++++++++ src/containers/Token/index.tsx | 44 +++---- src/containers/Token/test/index.test.tsx | 54 +++++---- src/rippled/{token.ts => token.js} | 25 +--- src/rootReducer.js | 5 + 11 files changed, 371 insertions(+), 86 deletions(-) create mode 100644 src/containers/Token/TokenHeader/actionTypes.js create mode 100644 src/containers/Token/TokenHeader/actions.js create mode 100644 src/containers/Token/TokenHeader/reducer.js create mode 100644 src/containers/Token/TokenHeader/test/actions.test.js create mode 100644 src/containers/Token/TokenHeader/test/reducer.test.js rename src/rippled/{token.ts => token.js} (74%) diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index 479628594..1b5734800 100644 --- a/src/containers/App/index.tsx +++ b/src/containers/App/index.tsx @@ -34,7 +34,7 @@ import { Transaction } from '../Transactions' import { Network } from '../Network' import { Validator } from '../Validators' import { PayString } from '../PayStrings' -import { Token } from '../Token' +import Token from '../Token' import { NFT } from '../NFT/NFT' import { legacyRedirect } from './legacyRedirects' import { useCustomNetworks } from '../shared/hooks' diff --git a/src/containers/Token/TokenHeader/actionTypes.js b/src/containers/Token/TokenHeader/actionTypes.js new file mode 100644 index 000000000..c539d0ce4 --- /dev/null +++ b/src/containers/Token/TokenHeader/actionTypes.js @@ -0,0 +1,4 @@ +export const START_LOADING_ACCOUNT_STATE = 'START_LOADING_ACCOUNT_STATE' +export const FINISHED_LOADING_ACCOUNT_STATE = 'FINISHED_LOADING_ACCOUNT_STATE' +export const ACCOUNT_STATE_LOAD_SUCCESS = 'ACCOUNT_STATE_LOAD_SUCCESS' +export const ACCOUNT_STATE_LOAD_FAIL = 'ACCOUNT_STATE_LOAD_FAIL' diff --git a/src/containers/Token/TokenHeader/actions.js b/src/containers/Token/TokenHeader/actions.js new file mode 100644 index 000000000..0d56b287d --- /dev/null +++ b/src/containers/Token/TokenHeader/actions.js @@ -0,0 +1,43 @@ +import { isValidClassicAddress, isValidXAddress } from 'ripple-address-codec' +import { getToken } from '../../../rippled' +import { analytics } from '../../shared/analytics' +import { BAD_REQUEST } from '../../shared/utils' +import * as actionTypes from './actionTypes' + +export const loadTokenState = + (currency, accountId, rippledSocket) => (dispatch) => { + if (!isValidClassicAddress(accountId) && !isValidXAddress(accountId)) { + dispatch({ + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + status: BAD_REQUEST, + error: '', + }) + return Promise.resolve() + } + + dispatch({ + type: actionTypes.START_LOADING_ACCOUNT_STATE, + }) + return getToken(currency, accountId, rippledSocket) + .then((data) => { + dispatch({ type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE }) + dispatch({ + type: actionTypes.ACCOUNT_STATE_LOAD_SUCCESS, + data, + }) + }) + .catch((error) => { + const status = error.code + analytics.trackException( + `token ${currency}.${accountId} --- ${JSON.stringify(error)}`, + ) + dispatch({ type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE }) + dispatch({ + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + error: status === 500 ? 'get_account_state_failed' : '', + status, + }) + }) + } + +export default loadTokenState diff --git a/src/containers/Token/TokenHeader/index.tsx b/src/containers/Token/TokenHeader/index.tsx index a3b3ddcfc..f529da88a 100644 --- a/src/containers/Token/TokenHeader/index.tsx +++ b/src/containers/Token/TokenHeader/index.tsx @@ -1,6 +1,12 @@ +import { useContext, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' +import { loadTokenState } from './actions' +import { Loader } from '../../shared/components/Loader' import './styles.scss' import { localizeNumber, formatLargeNumber } from '../../shared/utils' +import SocketContext from '../../shared/SocketContext' import Currency from '../../shared/components/Currency' import { Account } from '../../shared/components/Account' import DomainLink from '../../shared/components/DomainLink' @@ -8,7 +14,6 @@ import { TokenTableRow } from '../../shared/components/TokenTableRow' import { useLanguage } from '../../shared/hooks' import { LEDGER_ROUTE, TRANSACTION_ROUTE } from '../../App/routes' import { RouteLink } from '../../shared/routing' -import { TokenData } from '../../../rippled/token' const CURRENCY_OPTIONS = { style: 'currency', @@ -18,21 +23,44 @@ const CURRENCY_OPTIONS = { } interface TokenHeaderProps { + loading: boolean accountId: string currency: string - data: TokenData + data: { + balance: string + reserve: number + sequence: number + rate: number + obligations: string + domain: string + emailHash: string + previousLedger: number + previousTxn: string + flags: string[] + } + actions: { + loadTokenState: typeof loadTokenState + } } -export const TokenHeader = ({ +const TokenHeader = ({ + actions, accountId, currency, data, + loading, }: TokenHeaderProps) => { const language = useLanguage() const { t } = useTranslation() - const { domain, rate, emailHash, previousLedger, previousTxn } = data + const rippledSocket = useContext(SocketContext) + + useEffect(() => { + actions.loadTokenState(currency, accountId, rippledSocket) + }, [accountId, actions, currency, rippledSocket]) const renderDetails = () => { + const { domain, rate, emailHash, previousLedger, previousTxn } = data + const prevTxn = previousTxn && previousTxn.replace(/(.{20})..+/, '$1...') const abbrvEmail = emailHash && emailHash.replace(/(.{20})..+/, '$1...') return ( @@ -128,9 +156,7 @@ export const TokenHeader = ({ language, CURRENCY_OPTIONS, ) - const obligationsBalance = formatLargeNumber( - Number.parseFloat(obligations || '0'), - ) + const obligationsBalance = formatLargeNumber(Number.parseFloat(obligations)) return (
@@ -175,6 +201,7 @@ export const TokenHeader = ({ ) } + const { emailHash } = data return (
@@ -186,7 +213,24 @@ export const TokenHeader = ({ /> )}
-
{renderHeaderContent()}
+
+ {loading ? : renderHeaderContent()} +
) } + +export default connect( + (state: any) => ({ + loading: state.tokenHeader.loading, + data: state.tokenHeader.data, + }), + (dispatch) => ({ + actions: bindActionCreators( + { + loadTokenState, + }, + dispatch, + ), + }), +)(TokenHeader) diff --git a/src/containers/Token/TokenHeader/reducer.js b/src/containers/Token/TokenHeader/reducer.js new file mode 100644 index 000000000..b4768d53b --- /dev/null +++ b/src/containers/Token/TokenHeader/reducer.js @@ -0,0 +1,33 @@ +import * as actionTypes from './actionTypes' + +export const initialState = { + loading: false, + data: {}, + error: '', + status: null, +} + +// eslint-disable-next-line default-param-last +const tokenReducer = (state = initialState, action) => { + switch (action.type) { + case actionTypes.START_LOADING_ACCOUNT_STATE: + return { ...state, loading: true } + case actionTypes.FINISHED_LOADING_ACCOUNT_STATE: + return { ...state, loading: false } + case actionTypes.ACCOUNT_STATE_LOAD_SUCCESS: + return { ...state, error: '', data: action.data } + case actionTypes.ACCOUNT_STATE_LOAD_FAIL: + return { + ...state, + error: action.error, + status: action.status, + data: state.data.length ? state.data : {}, + } + case 'persist/REHYDRATE': + return { ...initialState } + default: + return state + } +} + +export default tokenReducer diff --git a/src/containers/Token/TokenHeader/test/actions.test.js b/src/containers/Token/TokenHeader/test/actions.test.js new file mode 100644 index 000000000..bfae5b48a --- /dev/null +++ b/src/containers/Token/TokenHeader/test/actions.test.js @@ -0,0 +1,108 @@ +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import * as actions from '../actions' +import * as actionTypes from '../actionTypes' +import { initialState } from '../reducer' +import { NOT_FOUND, BAD_REQUEST, SERVER_ERROR } from '../../../shared/utils' +import rippledResponses from './rippledResponses.json' +import actNotFound from './actNotFound.json' +import MockWsClient from '../../../test/mockWsClient' + +const TEST_ADDRESS = 'rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv' +const TEST_CURRENCY = 'abc' + +describe('TokenHeader Actions', () => { + jest.setTimeout(10000) + + const middlewares = [thunk] + const mockStore = configureMockStore(middlewares) + let client + beforeEach(() => { + client = new MockWsClient() + }) + + afterEach(() => { + client.close() + }) + + it('should dispatch correct actions on successful loadTokenState', () => { + client.addResponses(rippledResponses) + const expectedData = { + name: undefined, + obligations: '100', + sequence: 2148991, + reserve: 10, + rate: undefined, + domain: undefined, + emailHash: undefined, + flags: [], + balance: '123456000', + previousTxn: + '6B6F2CA1633A22247058E988372BA9EFFFC5BF10212230B67341CA32DC9D4A82', + previousLedger: 68990183, + } + const expectedActions = [ + { type: actionTypes.START_LOADING_ACCOUNT_STATE }, + { type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE }, + { type: actionTypes.ACCOUNT_STATE_LOAD_SUCCESS, data: expectedData }, + ] + const store = mockStore({ news: initialState }) + return store + .dispatch(actions.loadTokenState(TEST_CURRENCY, TEST_ADDRESS, client)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + it('should dispatch correct actions on server error', () => { + client.setReturnError() + const expectedActions = [ + { type: actionTypes.START_LOADING_ACCOUNT_STATE }, + { type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE }, + { + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + status: SERVER_ERROR, + error: 'get_account_state_failed', + }, + ] + const store = mockStore({ news: initialState }) + return store + .dispatch(actions.loadTokenState(TEST_CURRENCY, TEST_ADDRESS, client)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + it('should dispatch correct actions on ripple address not found', () => { + client.addResponse('account_info', { result: actNotFound }) + const expectedActions = [ + { type: actionTypes.START_LOADING_ACCOUNT_STATE }, + { type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE }, + { + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + status: NOT_FOUND, + error: '', + }, + ] + const store = mockStore({ news: initialState }) + return store + .dispatch(actions.loadTokenState(TEST_CURRENCY, TEST_ADDRESS, client)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions) + }) + }) + + it('should dispatch correct actions on invalid ripple address', () => { + const expectedActions = [ + { + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + status: BAD_REQUEST, + error: '', + }, + ] + const store = mockStore({ news: initialState }) + store.dispatch(actions.loadTokenState('ZZZ', null, client)).then(() => { + expect(store.getActions()).toEqual(expectedActions) + }) + }) +}) diff --git a/src/containers/Token/TokenHeader/test/reducer.test.js b/src/containers/Token/TokenHeader/test/reducer.test.js new file mode 100644 index 000000000..ceecc491f --- /dev/null +++ b/src/containers/Token/TokenHeader/test/reducer.test.js @@ -0,0 +1,79 @@ +import * as actionTypes from '../actionTypes' +import reducer, { initialState } from '../reducer' + +describe('AccountHeader reducers', () => { + it('should return the initial state', () => { + expect(reducer(undefined, {})).toEqual(initialState) + }) + + it('should handle START_LOADING_ACCOUNT_STATE', () => { + const nextState = { ...initialState, loading: true } + expect( + reducer(initialState, { type: actionTypes.START_LOADING_ACCOUNT_STATE }), + ).toEqual(nextState) + }) + + it('should handle FINISHED_LOADING_ACCOUNT_STATE', () => { + const nextState = { ...initialState, loading: false } + expect( + reducer(initialState, { + type: actionTypes.FINISHED_LOADING_ACCOUNT_STATE, + }), + ).toEqual(nextState) + }) + + it('should handle ACCOUNT_STATE_LOAD_SUCCESS', () => { + const data = [['XRP', 123.456]] + const nextState = { ...initialState, data } + expect( + reducer(initialState, { + data, + type: actionTypes.ACCOUNT_STATE_LOAD_SUCCESS, + }), + ).toEqual(nextState) + }) + + it('should handle ACCOUNT_STATE_LOAD_FAIL', () => { + const status = 500 + const error = 'error' + const nextState = { ...initialState, status, error } + expect( + reducer(initialState, { + status, + error, + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + }), + ).toEqual(nextState) + }) + + it('will not clear previous data on ACCOUNT_STATE_LOAD_FAIL', () => { + const data = [['XRP', 123.456]] + const error = 'error' + const status = 500 + const stateWithData = { ...initialState, data } + const nextState = { ...stateWithData, error, status } + expect( + reducer(stateWithData, { + status, + error, + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + }), + ).toEqual(nextState) + }) + + it('should clear data on rehydration', () => { + const error = 'error' + const status = 500 + const nextState = { ...initialState, error, status } + expect( + reducer(initialState, { + type: actionTypes.ACCOUNT_STATE_LOAD_FAIL, + error, + status, + }), + ).toEqual(nextState) + expect(reducer(nextState, { type: 'persist/REHYDRATE' })).toEqual( + initialState, + ) + }) +}) diff --git a/src/containers/Token/index.tsx b/src/containers/Token/index.tsx index 00362cf0e..09310ac75 100644 --- a/src/containers/Token/index.tsx +++ b/src/containers/Token/index.tsx @@ -1,9 +1,9 @@ -import { FC, PropsWithChildren, useContext, useEffect } from 'react' +import { FC, PropsWithChildren, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' import { Helmet } from 'react-helmet-async' -import { useQuery } from 'react-query' -import { TokenHeader } from './TokenHeader' +import TokenHeader from './TokenHeader' import { TokenTransactionTable } from './TokenTransactionTable' import { DEXPairs } from './DEXPairs' import NoMatch from '../NoMatch' @@ -14,9 +14,6 @@ import { useAnalytics } from '../shared/analytics' import { ErrorMessages } from '../shared/Interfaces' import { TOKEN_ROUTE } from '../App/routes' import { useRouteParams } from '../shared/routing' -import { getToken } from '../../rippled' -import SocketContext from '../shared/SocketContext' -import { Loader } from '../shared/components/Loader' const IS_MAINNET = process.env.VITE_ENVIRONMENT === 'mainnet' @@ -48,20 +45,11 @@ const Page: FC> = ({
) -export const Token = () => { - const rippledSocket = useContext(SocketContext) +const Token: FC<{ error: string }> = ({ error }) => { const { trackScreenLoaded } = useAnalytics() const { token = '' } = useRouteParams(TOKEN_ROUTE) const [currency, accountId] = token.split('.') const { t } = useTranslation() - const { - data: tokenData, - error: tokenDataError, - isLoading: isTokenDataLoading, - } = useQuery({ - queryKey: ['token', currency, accountId], - queryFn: () => getToken(currency, accountId, rippledSocket), - }) useEffect(() => { trackScreenLoaded({ @@ -75,31 +63,21 @@ export const Token = () => { }, [accountId, currency, trackScreenLoaded]) const renderError = () => { - const message = getErrorMessage(tokenDataError) + const message = getErrorMessage(error) return } - if (tokenDataError) { + if (error) { return {renderError()} } return ( - {isTokenDataLoading ? ( - - ) : ( - tokenData && ( - - ) - )} - {accountId && tokenData && IS_MAINNET && ( + {accountId && } + {accountId && IS_MAINNET && ( )} - {accountId && tokenData && ( + {accountId && (

{t('token_transactions')}

@@ -113,3 +91,7 @@ export const Token = () => { ) } + +export default connect((state: any) => ({ + error: state.accountHeader.status, +}))(Token) diff --git a/src/containers/Token/test/index.test.tsx b/src/containers/Token/test/index.test.tsx index 2ff37ad5a..c2f749d62 100644 --- a/src/containers/Token/test/index.test.tsx +++ b/src/containers/Token/test/index.test.tsx @@ -1,32 +1,33 @@ import { mount } from 'enzyme' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { Provider } from 'react-redux' import { Route } from 'react-router-dom' +import { initialState } from '../../../rootReducer' import i18n from '../../../i18n/testConfig' -import { Token } from '../index' -import { TokenHeader } from '../TokenHeader' +import Token from '../index' +import TokenHeader from '../TokenHeader' import { TokenTransactionTable } from '../TokenTransactionTable' -import { flushPromises, QuickHarness } from '../../test/utils' +import mockAccountState from '../../Accounts/test/mockAccountState.json' +import { QuickHarness } from '../../test/utils' import { TOKEN_ROUTE } from '../../App/routes' -import mockAccount from '../../Accounts/test/mockAccountState.json' -import Mock = jest.Mock -import { getToken } from '../../../rippled' - -jest.mock('../../../rippled', () => ({ - __esModule: true, - getToken: jest.fn(), -})) describe('Token container', () => { const TEST_ACCOUNT_ID = 'rTEST_ACCOUNT' - const createWrapper = (getAccountImpl = () => new Promise(() => {})) => { - ;(getToken as Mock).mockImplementation(getAccountImpl) + const middlewares = [thunk] + const mockStore = configureMockStore(middlewares) + const createWrapper = (state = {}) => { + const store = mockStore({ ...initialState, ...state }) return mount( - - } /> - , + + + } /> + + , ) } @@ -35,10 +36,17 @@ describe('Token container', () => { wrapper.unmount() }) - it('renders static parts', async () => { - const wrapper = createWrapper(() => Promise.resolve(mockAccount)) - await flushPromises() - wrapper.update() + it('renders static parts', () => { + const state = { + ...initialState, + accountHeader: { + loading: false, + error: null, + data: mockAccountState, + }, + } + + const wrapper = createWrapper(state) expect(wrapper.find(TokenHeader).length).toBe(1) expect(wrapper.find(TokenTransactionTable).length).toBe(1) wrapper.unmount() diff --git a/src/rippled/token.ts b/src/rippled/token.js similarity index 74% rename from src/rippled/token.ts rename to src/rippled/token.js index 9f6d8736d..aedc188c2 100644 --- a/src/rippled/token.ts +++ b/src/rippled/token.js @@ -4,26 +4,7 @@ import { getBalances, getAccountInfo, getServerInfo } from './lib/rippled' const log = logger({ name: 'iou' }) -export interface TokenData { - name: string - balance: string - reserve: number - sequence: number - gravatar: string - rate?: number - obligations?: string - domain?: string - emailHash?: string - previousLedger: number - previousTxn: string - flags: string[] -} - -const getToken = async ( - currencyCode, - issuer, - rippledSocket, -): Promise => { +const getToken = async (currencyCode, issuer, rippledSocket) => { try { log.info('fetching account info from rippled') const accountInfo = await getAccountInfo(rippledSocket, issuer) @@ -66,9 +47,7 @@ const getToken = async ( previousLedger, } } catch (error) { - if (error) { - log.error(error.toString()) - } + log.error(error.toString()) throw error } } diff --git a/src/rootReducer.js b/src/rootReducer.js index 0ad9a2f70..69ab09b9a 100644 --- a/src/rootReducer.js +++ b/src/rootReducer.js @@ -2,13 +2,18 @@ import { combineReducers } from 'redux' import accountHeaderReducer, { initialState as accountHeaderState, } from './containers/Accounts/AccountHeader/reducer' +import tokenHeaderReducer, { + initialState as tokenHeaderState, +} from './containers/Token/TokenHeader/reducer' export const initialState = { accountHeader: accountHeaderState, + tokenHeader: tokenHeaderState, } const rootReducer = combineReducers({ accountHeader: accountHeaderReducer, + tokenHeader: tokenHeaderReducer, }) export default rootReducer