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