diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index 1b5734800..479628594 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/DEXPairs/index.tsx b/src/containers/Token/DEXPairs/index.tsx index 84d6fd8bd..aa5320e62 100644 --- a/src/containers/Token/DEXPairs/index.tsx +++ b/src/containers/Token/DEXPairs/index.tsx @@ -169,7 +169,9 @@ export const DEXPairs = ({ accountId, currency }: DexPairsProps) => { - + diff --git a/src/containers/Token/TokenHeader/actionTypes.js b/src/containers/Token/TokenHeader/actionTypes.js deleted file mode 100644 index c539d0ce4..000000000 --- a/src/containers/Token/TokenHeader/actionTypes.js +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 0d56b287d..000000000 --- a/src/containers/Token/TokenHeader/actions.js +++ /dev/null @@ -1,43 +0,0 @@ -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 f529da88a..f46968a02 100644 --- a/src/containers/Token/TokenHeader/index.tsx +++ b/src/containers/Token/TokenHeader/index.tsx @@ -1,12 +1,7 @@ -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' @@ -14,6 +9,7 @@ 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', @@ -23,44 +19,21 @@ const CURRENCY_OPTIONS = { } interface TokenHeaderProps { - loading: boolean accountId: string currency: string - 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 - } + data: TokenData } -const TokenHeader = ({ - actions, +export const TokenHeader = ({ accountId, currency, data, - loading, }: TokenHeaderProps) => { const language = useLanguage() const { t } = useTranslation() - const rippledSocket = useContext(SocketContext) - - useEffect(() => { - actions.loadTokenState(currency, accountId, rippledSocket) - }, [accountId, actions, currency, rippledSocket]) + const { domain, rate, emailHash, previousLedger, previousTxn } = data const renderDetails = () => { - const { domain, rate, emailHash, previousLedger, previousTxn } = data - const prevTxn = previousTxn && previousTxn.replace(/(.{20})..+/, '$1...') const abbrvEmail = emailHash && emailHash.replace(/(.{20})..+/, '$1...') return ( @@ -156,7 +129,9 @@ const TokenHeader = ({ language, CURRENCY_OPTIONS, ) - const obligationsBalance = formatLargeNumber(Number.parseFloat(obligations)) + const obligationsBalance = formatLargeNumber( + Number.parseFloat(obligations || 0), + ) return (
@@ -201,7 +176,6 @@ const TokenHeader = ({ ) } - const { emailHash } = data return (
@@ -213,24 +187,7 @@ const TokenHeader = ({ /> )}
-
- {loading ? : renderHeaderContent()} -
+
{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 deleted file mode 100644 index b4768d53b..000000000 --- a/src/containers/Token/TokenHeader/reducer.js +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index bfae5b48a..000000000 --- a/src/containers/Token/TokenHeader/test/actions.test.js +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index ceecc491f..000000000 --- a/src/containers/Token/TokenHeader/test/reducer.test.js +++ /dev/null @@ -1,79 +0,0 @@ -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 09310ac75..84f7e87f1 100644 --- a/src/containers/Token/index.tsx +++ b/src/containers/Token/index.tsx @@ -1,9 +1,9 @@ -import { FC, PropsWithChildren, useEffect } from 'react' +import { FC, PropsWithChildren, useContext, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { connect } from 'react-redux' import { Helmet } from 'react-helmet-async' -import TokenHeader from './TokenHeader' +import { useQuery } from 'react-query' +import { TokenHeader } from './TokenHeader' import { TokenTransactionTable } from './TokenTransactionTable' import { DEXPairs } from './DEXPairs' import NoMatch from '../NoMatch' @@ -14,6 +14,9 @@ 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' @@ -45,11 +48,20 @@ const Page: FC> = ({
) -const Token: FC<{ error: string }> = ({ error }) => { +export const Token: FC<{ error: string }> = () => { + const rippledSocket = useContext(SocketContext) 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({ @@ -63,21 +75,29 @@ const Token: FC<{ error: string }> = ({ error }) => { }, [accountId, currency, trackScreenLoaded]) const renderError = () => { - const message = getErrorMessage(error) + const message = getErrorMessage(tokenDataError) return } - if (error) { + if (tokenDataError) { return {renderError()} } return ( - {accountId && } - {accountId && IS_MAINNET && ( + {isTokenDataLoading ? ( + + ) : ( + + )} + {accountId && tokenData && IS_MAINNET && ( )} - {accountId && ( + {accountId && tokenData && (

{t('token_transactions')}

@@ -91,7 +111,3 @@ const Token: FC<{ error: string }> = ({ error }) => { ) } - -export default connect((state: any) => ({ - error: state.accountHeader.status, -}))(Token) diff --git a/src/rippled/token.js b/src/rippled/token.ts similarity index 74% rename from src/rippled/token.js rename to src/rippled/token.ts index aedc188c2..9f6d8736d 100644 --- a/src/rippled/token.js +++ b/src/rippled/token.ts @@ -4,7 +4,26 @@ import { getBalances, getAccountInfo, getServerInfo } from './lib/rippled' const log = logger({ name: 'iou' }) -const getToken = async (currencyCode, issuer, rippledSocket) => { +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 => { try { log.info('fetching account info from rippled') const accountInfo = await getAccountInfo(rippledSocket, issuer) @@ -47,7 +66,9 @@ const getToken = async (currencyCode, issuer, rippledSocket) => { previousLedger, } } catch (error) { - log.error(error.toString()) + if (error) { + log.error(error.toString()) + } throw error } } diff --git a/src/rootReducer.js b/src/rootReducer.js index 69ab09b9a..0ad9a2f70 100644 --- a/src/rootReducer.js +++ b/src/rootReducer.js @@ -2,18 +2,13 @@ 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
{t('pair')} + {t('pair')} + {t('issuer')} {t('offer_range')}