diff --git a/.changelog/2064.internal.md b/.changelog/2064.internal.md new file mode 100644 index 0000000000..f551f8dc3d --- /dev/null +++ b/.changelog/2064.internal.md @@ -0,0 +1 @@ +Delay GetEpoch request until needed diff --git a/src/app/components/AddEscrowForm/__tests__/__snapshots__/index.test.tsx.snap b/src/app/components/AddEscrowForm/__tests__/__snapshots__/index.test.tsx.snap index f0a7740ba3..4ffafdb684 100644 --- a/src/app/components/AddEscrowForm/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/components/AddEscrowForm/__tests__/__snapshots__/index.test.tsx.snap @@ -316,14 +316,16 @@ exports[` should match snapshot 1`] = ` > + > + TEST + should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/components/ReclaimEscrowForm/__tests__/__snapshots__/index.test.tsx.snap b/src/app/components/ReclaimEscrowForm/__tests__/__snapshots__/index.test.tsx.snap index 7815269f8c..33e6f46326 100644 --- a/src/app/components/ReclaimEscrowForm/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/components/ReclaimEscrowForm/__tests__/__snapshots__/index.test.tsx.snap @@ -435,7 +435,9 @@ exports[` should match snapshot 1`] = ` > + > + TEST + should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/components/TimeToEpoch/__tests__/index.test.tsx b/src/app/components/TimeToEpoch/__tests__/index.test.tsx index e579a5b11f..607dd4e142 100644 --- a/src/app/components/TimeToEpoch/__tests__/index.test.tsx +++ b/src/app/components/TimeToEpoch/__tests__/index.test.tsx @@ -1,34 +1,16 @@ -import * as React from 'react' import { render } from '@testing-library/react' -import { Provider } from 'react-redux' -import { configureAppStore } from 'store/configureStore' -import { NetworkState } from 'app/state/network/types' import { TimeToEpoch } from '..' -const renderComponent = (store: any, epoch: number) => - render( - - - , - ) +const renderComponent = (currentEpoch: number, epoch: number) => + render() describe('', () => { - let store: ReturnType - - beforeEach(() => { - store = configureAppStore({ - network: { - epoch: 10000, - } as NetworkState, - }) - }) - it('should estimate debonding times', () => { - expect(renderComponent(store, 10336).container.textContent).toEqual('in 14 days') - expect(renderComponent(store, 10306).container.textContent).toEqual('in 13 days') - expect(renderComponent(store, 10086).container.textContent).toEqual('in 4 days') - expect(renderComponent(store, 10047).container.textContent).toEqual('in 47 hours') - expect(renderComponent(store, 10001).container.textContent).toEqual('in 1 hour') + expect(renderComponent(10000, 10336).container.textContent).toEqual('in 14 days') + expect(renderComponent(10000, 10306).container.textContent).toEqual('in 13 days') + expect(renderComponent(10000, 10086).container.textContent).toEqual('in 4 days') + expect(renderComponent(10000, 10047).container.textContent).toEqual('in 47 hours') + expect(renderComponent(10000, 10001).container.textContent).toEqual('in 1 hour') }) }) diff --git a/src/app/components/TimeToEpoch/index.tsx b/src/app/components/TimeToEpoch/index.tsx index d3cbc46b29..9ec5944e39 100644 --- a/src/app/components/TimeToEpoch/index.tsx +++ b/src/app/components/TimeToEpoch/index.tsx @@ -1,8 +1,7 @@ -import { selectEpoch } from 'app/state/network/selectors' -import { useSelector } from 'react-redux' import * as React from 'react' interface Props { + currentEpoch?: number epoch: number } @@ -11,8 +10,10 @@ const estimatedEpochsPerHour = 1 const relativeFormat = new Intl.RelativeTimeFormat(process?.env?.NODE_ENV === 'test' ? 'en-US' : undefined) export function TimeToEpoch(props: Props) { - const currentEpoch = useSelector(selectEpoch) - const remainingHours = (props.epoch - currentEpoch) / estimatedEpochsPerHour + if (!props.currentEpoch) { + return null + } + const remainingHours = (props.epoch - props.currentEpoch) / estimatedEpochsPerHour // TODO: add more thresholds if used for other than debonding const formattedRemainingTime = diff --git a/src/app/components/Toolbar/Features/Account/__tests__/__snapshots__/Account.test.tsx.snap b/src/app/components/Toolbar/Features/Account/__tests__/__snapshots__/Account.test.tsx.snap index ad27c50f3e..bbeadcf737 100644 --- a/src/app/components/Toolbar/Features/Account/__tests__/__snapshots__/Account.test.tsx.snap +++ b/src/app/components/Toolbar/Features/Account/__tests__/__snapshots__/Account.test.tsx.snap @@ -531,7 +531,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/components/Toolbar/Features/AccountSelector/__tests__/__snapshots__/index.test.tsx.snap b/src/app/components/Toolbar/Features/AccountSelector/__tests__/__snapshots__/index.test.tsx.snap index c2829b263b..e7734352c3 100644 --- a/src/app/components/Toolbar/Features/AccountSelector/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/components/Toolbar/Features/AccountSelector/__tests__/__snapshots__/index.test.tsx.snap @@ -750,7 +750,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/pages/AccountPage/__tests__/__snapshots__/index.test.tsx.snap b/src/app/pages/AccountPage/__tests__/__snapshots__/index.test.tsx.snap index 398b5025e8..bff8311e30 100644 --- a/src/app/pages/AccountPage/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/pages/AccountPage/__tests__/__snapshots__/index.test.tsx.snap @@ -966,7 +966,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST @@ -989,7 +989,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST @@ -1013,7 +1013,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST @@ -1036,7 +1036,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST @@ -1123,7 +1123,9 @@ exports[` should match snapshot 1`] = ` > + > + TEST + should match snapshot 1`] = ` `; -exports[` with missing delegations 1`] = `"Total-Available100.0 Staked-Debonding-"`; +exports[` with missing delegations 1`] = `"Total-Available100.0 TEST Staked-Debonding-"`; diff --git a/src/app/pages/OpenWalletPage/Features/FromMnemonic/__tests__/index.test.tsx b/src/app/pages/OpenWalletPage/Features/FromMnemonic/__tests__/index.test.tsx index fe67fb1680..2dccd75ffb 100644 --- a/src/app/pages/OpenWalletPage/Features/FromMnemonic/__tests__/index.test.tsx +++ b/src/app/pages/OpenWalletPage/Features/FromMnemonic/__tests__/index.test.tsx @@ -7,6 +7,12 @@ import { Provider } from 'react-redux' import { ThemeProvider } from 'styles/theme/ThemeProvider' import { FromMnemonic } from '..' +const mockDispatch = jest.fn() +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})) + const renderPage = (store: any) => render( @@ -58,7 +64,7 @@ describe('', () => { expect(errorElem).toBeNull() }) - it('should display account selection modal window', async () => { + it('should call the success handler and dispatch Redux action', async () => { renderPage(store) const textbox = screen.getByRole('textbox') as HTMLInputElement const button = screen.getByRole('button', { name: 'openWallet.mnemonic.import' }) @@ -66,6 +72,9 @@ describe('', () => { await userEvent.type(textbox, 'echo toward hold roast rather reduce cute civil equal whale wait conduct') await userEvent.click(button) - expect(await screen.findByText('openWallet.importAccounts.selectWallets')).toBeInTheDocument() + expect(mockDispatch).toHaveBeenCalledWith({ + payload: 'echo toward hold roast rather reduce cute civil equal whale wait conduct', + type: 'importAccounts/enumerateAccountsFromMnemonic', + }) }) }) diff --git a/src/app/pages/StakingPage/Features/CommissionBounds/__tests__/index.test.tsx b/src/app/pages/StakingPage/Features/CommissionBounds/__tests__/index.test.tsx index 6d44630706..8c2d566959 100644 --- a/src/app/pages/StakingPage/Features/CommissionBounds/__tests__/index.test.tsx +++ b/src/app/pages/StakingPage/Features/CommissionBounds/__tests__/index.test.tsx @@ -21,7 +21,11 @@ describe('', () => { let store: ReturnType beforeEach(() => { - store = configureAppStore() + store = configureAppStore({ + network: { + epoch: 0, + } as NetworkState, + }) }) it('should match snapshot when empty', () => { @@ -43,7 +47,7 @@ describe('', () => { store = configureAppStore() const component = renderComponent(store, [{ epochStart: 0, lower: 0.1, upper: 0.2, epochEnd: 100 }]) act(() => { - store.dispatch(networkActions.networkSelected({ epoch: 50 } as NetworkState)) + store.dispatch(networkActions.setEpoch(50)) }) expect(component.baseElement).toMatchSnapshot() }) diff --git a/src/app/pages/StakingPage/Features/CommissionBounds/index.tsx b/src/app/pages/StakingPage/Features/CommissionBounds/index.tsx index 1d287e675f..b087dbd9dc 100644 --- a/src/app/pages/StakingPage/Features/CommissionBounds/index.tsx +++ b/src/app/pages/StakingPage/Features/CommissionBounds/index.tsx @@ -14,7 +14,9 @@ interface CommissionBoundProps { const CommissionBound = memo((props: CommissionBoundProps) => { const { t } = useTranslation() const epoch = useSelector(selectEpoch) - + if (typeof epoch !== 'number') { + return null + } const bound = props.bound const isCurrentBounds = epoch > bound.epochStart && (!bound.epochEnd || epoch < bound.epochEnd) diff --git a/src/app/pages/StakingPage/Features/DelegationList/DebondingDelegationList.tsx b/src/app/pages/StakingPage/Features/DelegationList/DebondingDelegationList.tsx index bb222f61d3..dbd4899a12 100644 --- a/src/app/pages/StakingPage/Features/DelegationList/DebondingDelegationList.tsx +++ b/src/app/pages/StakingPage/Features/DelegationList/DebondingDelegationList.tsx @@ -1,18 +1,28 @@ +import { useEffect } from 'react' import { selectDebondingDelegations } from 'app/state/staking/selectors' import { Box } from 'grommet/es6/components/Box' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { DelegationList } from '.' import { StakeSubnavigation } from '../../../AccountPage/Features/StakeSubnavigation' +import { networkActions } from '../../../../../app/state/network' +import { selectEpoch } from '../../../../../app/state/network/selectors' export const DebondingDelegationList = () => { + const dispatch = useDispatch() const delegations = useSelector(selectDebondingDelegations) + const currentEpoch = useSelector(selectEpoch) + + useEffect(() => { + dispatch(networkActions.getEpoch()) + }, [dispatch, currentEpoch]) + return ( <> - + diff --git a/src/app/pages/StakingPage/Features/DelegationList/__tests__/DebondingDelegationList.test.tsx b/src/app/pages/StakingPage/Features/DelegationList/__tests__/DebondingDelegationList.test.tsx index 2b5568f00b..b3ff81eb03 100644 --- a/src/app/pages/StakingPage/Features/DelegationList/__tests__/DebondingDelegationList.test.tsx +++ b/src/app/pages/StakingPage/Features/DelegationList/__tests__/DebondingDelegationList.test.tsx @@ -7,6 +7,7 @@ import { Provider } from 'react-redux' import { DebondingDelegationList } from '../DebondingDelegationList' import { configureAppStore } from 'store/configureStore' import { stakingActions } from 'app/state/staking' +import { NetworkState } from '../../../../../../app/state/network/types' import { ThemeProvider } from '../../../../../../styles/theme/ThemeProvider' import { MemoryRouter } from 'react-router-dom' @@ -25,7 +26,12 @@ describe('', () => { let store: ReturnType beforeEach(() => { - store = configureAppStore() + store = configureAppStore({ + network: { + ticker: 'TEST', + epoch: 4, + } as NetworkState, + }) }) it('should match snapshot', () => { diff --git a/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/ActiveDelegationList.test.tsx.snap b/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/ActiveDelegationList.test.tsx.snap index 9725aefaa4..fd7e926c1c 100644 --- a/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/ActiveDelegationList.test.tsx.snap +++ b/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/ActiveDelegationList.test.tsx.snap @@ -608,7 +608,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/DebondingDelegationList.test.tsx.snap b/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/DebondingDelegationList.test.tsx.snap index b9c2e4b30c..bcba34d80e 100644 --- a/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/DebondingDelegationList.test.tsx.snap +++ b/src/app/pages/StakingPage/Features/DelegationList/__tests__/__snapshots__/DebondingDelegationList.test.tsx.snap @@ -594,7 +594,7 @@ exports[` should match snapshot 1`] = ` class="notranslate" translate="no" > - + TEST diff --git a/src/app/pages/StakingPage/Features/DelegationList/index.tsx b/src/app/pages/StakingPage/Features/DelegationList/index.tsx index 6c11332900..e10dcbd3d9 100644 --- a/src/app/pages/StakingPage/Features/DelegationList/index.tsx +++ b/src/app/pages/StakingPage/Features/DelegationList/index.tsx @@ -30,6 +30,7 @@ type Props = } | { type: 'debonding' + currentEpoch?: number delegations: DebondingDelegation[] } @@ -141,7 +142,13 @@ export const DelegationList = memo((props: Props) => { id: 'debondingTimeEnd', selector: 'epoch', sortable: true, - cell: datum => , + cell: datum => { + if ('currentEpoch' in props) { + return ( + + ) + } + }, }, } diff --git a/src/app/state/network/index.ts b/src/app/state/network/index.ts index 2490d83d26..d87e7829da 100644 --- a/src/app/state/network/index.ts +++ b/src/app/state/network/index.ts @@ -17,16 +17,24 @@ export const networkSlice = createSlice({ initializeNetwork() {}, selectNetwork(state, action: PayloadAction) { state.chainContext = initialState.chainContext + state.epoch = initialState.epoch }, initialNetworkSelected(state, action: PayloadAction) { Object.assign(state, action.payload) }, networkSelected(state, action: PayloadAction) { - Object.assign(state, action.payload, { chainContext: initialState.chainContext }) + Object.assign(state, action.payload, { + chainContext: initialState.chainContext, + epoch: initialState.epoch, + }) }, setChainContext(state, action: PayloadAction) { state.chainContext = action.payload }, + setEpoch(state, action: PayloadAction) { + state.epoch = action.payload + }, + getEpoch(state) {}, }, }) diff --git a/src/app/state/network/saga.test.ts b/src/app/state/network/saga.test.ts index abef73d2fa..de1ef676d6 100644 --- a/src/app/state/network/saga.test.ts +++ b/src/app/state/network/saga.test.ts @@ -1,6 +1,6 @@ import { expectSaga, testSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' -import { getChainContext, getOasisNic, networkSaga, selectNetwork } from './saga' +import { getChainContext, getEpoch, getOasisNic, networkSaga, selectNetwork } from './saga' import { networkActions } from '.' describe('Network Sagas', () => { @@ -59,4 +59,40 @@ describe('Network Sagas', () => { .run() }) }) + + describe('getEpoch', () => { + const mockEpoch = 35337 + const mockSelectedNetwork = 'testnet' + const mockNic = { + beaconGetEpoch: jest.fn().mockResolvedValue(mockEpoch), + } + + it('should return existing epoch if available', () => { + return expectSaga(getEpoch) + .withState({ + network: { + epoch: mockEpoch, + selectedNetwork: mockSelectedNetwork, + }, + }) + .returns(mockEpoch) + .run() + }) + + it('should fetch and return epoch when it is missing in state', () => { + return expectSaga(getEpoch) + .withState({ + network: { + selectedNetwork: mockSelectedNetwork, + }, + }) + .provide([ + [matchers.call.fn(getOasisNic), mockNic], + [matchers.call.fn(mockNic.beaconGetEpoch), mockEpoch], + ]) + .put(networkActions.setEpoch(mockEpoch)) + .returns(mockEpoch) + .run() + }) + }) }) diff --git a/src/app/state/network/saga.ts b/src/app/state/network/saga.ts index 484350fb91..1514da069f 100644 --- a/src/app/state/network/saga.ts +++ b/src/app/state/network/saga.ts @@ -3,12 +3,12 @@ import { persistActions } from 'app/state/persist' import { selectSkipUnlockingOnInit } from 'app/state/persist/selectors' import { config } from 'config' import { RECEIVE_INIT_STATE } from 'redux-state-sync' -import { all, call, put, select, takeLatest } from 'typed-redux-saga' +import { call, put, select, takeLatest } from 'typed-redux-saga' import { backend, backendApi } from 'vendors/backend' import { networkActions } from '.' import { SyncedRootState } from '../persist/types' -import { selectChainContext, selectSelectedNetwork } from './selectors' +import { selectChainContext, selectEpoch, selectSelectedNetwork } from './selectors' import { NetworkType } from './types' import { WalletError, WalletErrors } from 'types/errors' @@ -50,6 +50,24 @@ export function* getChainContext() { } } +export function* getEpoch() { + const epoch = yield* select(selectEpoch) + if (epoch) { + return epoch + } + + try { + const selectedNetwork = yield* select(selectSelectedNetwork) + const nic = yield* call(getOasisNic, selectedNetwork) + const fetchedEpoch = yield* call([nic, nic.beaconGetEpoch], oasis.consensus.HEIGHT_LATEST) + const fetchedEpochNumber = Number(fetchedEpoch) + yield* put(networkActions.setEpoch(fetchedEpochNumber)) + return fetchedEpochNumber + } catch (error) { + throw new WalletError(WalletErrors.UnknownGrpcError, 'Could not fetch data') + } +} + export function* selectNetwork({ network, isInitializing, @@ -57,13 +75,8 @@ export function* selectNetwork({ network: NetworkType isInitializing: boolean }) { - const nic = yield* call(getOasisNic, network) - const { epoch } = yield* all({ - epoch: call([nic, nic.beaconGetEpoch], oasis.consensus.HEIGHT_LATEST), - }) const networkState = { ticker: config[network].ticker, - epoch: Number(epoch), // Will lose precision in a few billion years at 1 epoch per hour selectedNetwork: network, minimumStakingAmount: config[network].min_delegation, } @@ -113,5 +126,7 @@ export function* networkSaga() { } }) + yield* takeLatest(networkActions.getEpoch, getEpoch) + yield* put(networkActions.initializeNetwork()) } diff --git a/src/app/state/network/types.ts b/src/app/state/network/types.ts index ecdd00085b..0e4ea51bfb 100644 --- a/src/app/state/network/types.ts +++ b/src/app/state/network/types.ts @@ -12,7 +12,7 @@ export interface NetworkState { chainContext?: string /** Current epoch */ - epoch: number + epoch?: number /** Minimum staking amount */ minimumStakingAmount: number diff --git a/src/app/state/persist/syncTabs.ts b/src/app/state/persist/syncTabs.ts index 563fac0ea9..885fa01dc1 100644 --- a/src/app/state/persist/syncTabs.ts +++ b/src/app/state/persist/syncTabs.ts @@ -62,6 +62,8 @@ export const whitelistTabSyncActions: Record = { [rootSlices.wallet.actions.updateBalance.type]: true, [rootSlices.network.actions.networkSelected.type]: true, [rootSlices.network.actions.setChainContext.type]: true, + [rootSlices.network.actions.getEpoch.type]: true, + [rootSlices.network.actions.setEpoch.type]: true, [rootSlices.persist.actions.setUnlockedRootState.type]: true, [rootSlices.persist.actions.resetRootState.type]: true, [rootSlices.persist.actions.skipUnlocking.type]: true, diff --git a/src/app/state/staking/saga.test.ts b/src/app/state/staking/saga.test.ts index e497c46628..41a338e8e3 100644 --- a/src/app/state/staking/saga.test.ts +++ b/src/app/state/staking/saga.test.ts @@ -76,7 +76,11 @@ describe('Staking Sagas', () => { nic.stakingAccount.mockResolvedValue({}) return expectSaga(stakingSaga) - .withState({}) + .withState({ + network: { + epoch: 35337, + }, + }) .provide(providers) .dispatch(stakingActions.validatorSelected('oasis1qqzz2le7nua2hvrkjrc9kc6n08ycs9a80chejmr7')) .put.actionType(stakingActions.updateValidatorDetails.type) diff --git a/src/app/state/staking/saga.ts b/src/app/state/staking/saga.ts index 02b5f50c5a..86c7591394 100644 --- a/src/app/state/staking/saga.ts +++ b/src/app/state/staking/saga.ts @@ -8,8 +8,8 @@ import { WalletError, WalletErrors } from 'types/errors' import { parseValidatorsList } from 'vendors/oasisscan' import { stakingActions } from '.' -import { getExplorerAPIs, getOasisNic } from '../network/saga' -import { selectEpoch, selectSelectedNetwork } from '../network/selectors' +import { getEpoch, getExplorerAPIs, getOasisNic } from '../network/saga' +import { selectSelectedNetwork } from '../network/selectors' import { selectValidators, selectValidatorsNetwork } from './selectors' import { CommissionBound, DebondingDelegation, Delegation, Validators } from './types' @@ -156,7 +156,7 @@ export function* getValidatorDetails({ payload: address }: PayloadAction const nic = yield* call(getOasisNic) const publicKey = yield* call(addressToPublicKey, address) const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 }) - const currentEpoch = yield* select(selectEpoch) + const currentEpoch = yield* call(getEpoch) let rawBounds = account.escrow?.commission_schedule?.bounds if (!rawBounds) { diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index ebf8a3ceaa..65c24abba8 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -73,7 +73,7 @@ export const privateKeyUnlockedState = { ticker: 'ROSE', chainContext: '', selectedNetwork: 'mainnet', - epoch: 18372, + epoch: 0, minimumStakingAmount: 100, }, paraTimes: { @@ -274,7 +274,7 @@ export const walletExtensionV0UnlockedState = { chainContext: '', ticker: 'ROSE', selectedNetwork: 'mainnet', - epoch: 27884, + epoch: 0, minimumStakingAmount: 100, }, paraTimes: {