Skip to content

Commit

Permalink
feat: add PooledStaking slice for managing staking state (#12363)
Browse files Browse the repository at this point in the history
## **Description**

This PR refactors the staking-related state management by introducing a
staking redux slice. This change improves state management consistency,
reduces duplicate state, and makes the data flow more predictable across
the staking feature.

- Created new `PooledStaking` slice to manage staking-related state
- Refactored hooks to use Redux store instead of local state:
  - `usePooledStakes`
  - `useStakingEligibility`
  - `useVaultData`
  - `useStakingEarnings`
- Moved `hasStakedPositions` from `usePooledStakes` to
`useStakingEarnings` to avoid prop drilling
- Updated tests to reflect new state management pattern

## **Related issues**

Fixes:
[STAKE-870](https://consensyssoftware.atlassian.net/browse/STAKE-870)

## **Manual testing steps**

1. Perform an unstake transaction 
2. Navigate to Eth details page while the transaction is still
processing
3. You will now see the unstaking banner pop up when transaction is
completed without navigating away and coming back to the details page

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**


https://github.com/user-attachments/assets/cbdfad52-58ae-4396-a214-0c793eb49f7c


### **After**


https://github.com/user-attachments/assets/d064012f-32c3-41a3-8537-4593b6e409af


## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

+

[STAKE-870]:
https://consensyssoftware.atlassian.net/browse/STAKE-870?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
amitabh94 authored Nov 26, 2024
1 parent ece0220 commit 39536b5
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ jest.mock('@react-navigation/native', () => {
};
});

jest.mock('../../hooks/useStakingEligibility', () => ({
__esModule: true,
default: () => ({
isEligible: true,
loading: false,
error: null,
refreshPooledStakingEligibility: jest.fn(),
}),
}));

jest.mock('../../hooks/useStakingEarnings', () => ({
__esModule: true,
default: () => ({
Expand All @@ -38,6 +28,7 @@ jest.mock('../../hooks/useStakingEarnings', () => ({
estimatedAnnualEarningsETH: '2.5 ETH',
estimatedAnnualEarningsFiat: '$5000',
isLoadingEarningsData: false,
hasStakedPositions: true,
}),
}));

Expand Down
4 changes: 1 addition & 3 deletions app/components/UI/Stake/components/StakingEarnings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { isPooledStakingFeatureEnabled } from '../../../Stake/constants';
import useStakingChain from '../../hooks/useStakingChain';
import { StakeSDKProvider } from '../../sdk/stakeSdkProvider';
import useStakingEarnings from '../../hooks/useStakingEarnings';
import usePooledStakes from '../../hooks/usePooledStakes';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { withMetaMetrics } from '../../utils/metaMetrics/withMetaMetrics';
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
Expand All @@ -30,15 +29,14 @@ const StakingEarningsContent = () => {

const { openTooltipModal } = useTooltipModal();

const { hasStakedPositions } = usePooledStakes();

const {
annualRewardRate,
lifetimeRewardsETH,
lifetimeRewardsFiat,
estimatedAnnualEarningsETH,
estimatedAnnualEarningsFiat,
isLoadingEarningsData,
hasStakedPositions,
} = useStakingEarnings();

const onDisplayAnnualRateTooltip = () =>
Expand Down
139 changes: 62 additions & 77 deletions app/components/UI/Stake/hooks/usePooledStakes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useSelector } from 'react-redux';
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController';
import { selectChainId } from '../../../../selectors/networkController';
import { hexToNumber } from '@metamask/utils';
import { PooledStake } from '@metamask/stake-sdk';
import { useStakeContext } from './useStakeContext';
import {
selectPooledStakesData,
setPooledStakes,
} from '../../../../core/redux/slices/staking';

export enum StakeAccountStatus {
// These statuses are only used internally rather than displayed to a user
Expand All @@ -15,51 +19,48 @@ export enum StakeAccountStatus {
}

const usePooledStakes = () => {
const dispatch = useDispatch();
const chainId = useSelector(selectChainId);
const selectedAddress =
useSelector(selectSelectedInternalAccountChecksummedAddress) || '';
const { stakingApiService } = useStakeContext(); // Get the stakingApiService directly from context
const [pooledStakesData, setPooledStakesData] = useState({} as PooledStake);
const [exchangeRate, setExchangeRate] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);

if (!stakingApiService) {
throw new Error('Staking API service is unavailable');
}

const addresses = selectedAddress ? [selectedAddress] : [];
const numericChainId = hexToNumber(chainId);
const { pooledStakesData, exchangeRate } = useSelector(
selectPooledStakesData,
);
const { stakingApiService } = useStakeContext();

// Directly calling the stakingApiService
const { accounts = [], exchangeRate: fetchedExchangeRate } =
await stakingApiService.getPooledStakes(
addresses,
numericChainId,
true,
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

setPooledStakesData(accounts[0] || null);
setExchangeRate(fetchedExchangeRate);
} catch (err) {
setError('Failed to fetch pooled stakes');
} finally {
setLoading(false);
}
};
const fetchData = useCallback(async () => {
if (!stakingApiService || !selectedAddress) return;

setIsLoading(true);
setError(null);

try {
const { accounts = [], exchangeRate: fetchedExchangeRate } =
await stakingApiService.getPooledStakes(
[selectedAddress],
hexToNumber(chainId),
true,
);

dispatch(
setPooledStakes({
pooledStakes: accounts[0] || {},
exchangeRate: fetchedExchangeRate,
}),
);
} catch (err) {
setError('Failed to fetch pooled stakes');
} finally {
setIsLoading(false);
}
}, [chainId, selectedAddress, stakingApiService, dispatch]);

useEffect(() => {
fetchData();
}, [chainId, selectedAddress, stakingApiService, refreshKey]);

const refreshPooledStakes = () => {
setRefreshKey((prevKey) => prevKey + 1); // Increment `refreshKey` to trigger refetch
};
}, [fetchData]);

const getStatus = (stake: PooledStake) => {
if (stake.assets === '0' && stake.exitRequests.length > 0) {
Expand All @@ -72,48 +73,32 @@ const usePooledStakes = () => {
return StakeAccountStatus.ACTIVE;
};

const status = useMemo(() => getStatus(pooledStakesData), [pooledStakesData]);

const hasStakedPositions = useMemo(
() =>
status === StakeAccountStatus.ACTIVE ||
status === StakeAccountStatus.INACTIVE_WITH_EXIT_REQUESTS,
[status],
);

const hasRewards = useMemo(
() =>
status === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY ||
status === StakeAccountStatus.ACTIVE,
[status],
);

const hasRewardsOnly = useMemo(
() => status === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY,
[status],
);

const hasNeverStaked = useMemo(
() => status === StakeAccountStatus.NEVER_STAKED,
[status],
);

const hasEthToUnstake = useMemo(
() => status === StakeAccountStatus.ACTIVE,
[status],
);
const statusFlags = useMemo(() => {
const currentStatus = pooledStakesData
? getStatus(pooledStakesData)
: StakeAccountStatus.NEVER_STAKED;

return {
hasStakedPositions:
currentStatus === StakeAccountStatus.ACTIVE ||
currentStatus === StakeAccountStatus.INACTIVE_WITH_EXIT_REQUESTS,
hasRewards:
currentStatus === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY ||
currentStatus === StakeAccountStatus.ACTIVE,
hasRewardsOnly:
currentStatus === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY,
hasNeverStaked: currentStatus === StakeAccountStatus.NEVER_STAKED,
hasEthToUnstake: currentStatus === StakeAccountStatus.ACTIVE,
};
}, [pooledStakesData]);

return {
pooledStakesData,
exchangeRate,
isLoadingPooledStakesData: loading,
isLoadingPooledStakesData: isLoading,
error,
refreshPooledStakes,
hasStakedPositions,
hasEthToUnstake,
hasNeverStaked,
hasRewards,
hasRewardsOnly,
refreshPooledStakes: fetchData,
...statusFlags,
};
};

Expand Down
6 changes: 4 additions & 2 deletions app/components/UI/Stake/hooks/useStakingEarnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const useStakingEarnings = () => {

const { currentCurrency, conversionRate } = useBalance();

const { pooledStakesData, isLoadingPooledStakesData } = usePooledStakes();
const { pooledStakesData, isLoadingPooledStakesData, hasStakedPositions } =
usePooledStakes();

const lifetimeRewards = pooledStakesData?.lifetimeRewards ?? '0';

Expand All @@ -26,7 +27,7 @@ const useStakingEarnings = () => {
2,
);

const assets = pooledStakesData.assets ?? 0;
const assets = pooledStakesData?.assets ?? 0;
const estimatedAnnualEarnings = new BigNumber(assets)
.multipliedBy(annualRewardRateDecimal)
.toFixed(0);
Expand All @@ -50,6 +51,7 @@ const useStakingEarnings = () => {
estimatedAnnualEarningsETH,
estimatedAnnualEarningsFiat,
isLoadingEarningsData,
hasStakedPositions,
};
};

Expand Down
45 changes: 24 additions & 21 deletions app/components/UI/Stake/hooks/useStakingEligibility.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,54 @@
import { useSelector } from 'react-redux';
import { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useEffect, useCallback, useState } from 'react';
import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController';
import { useStakeContext } from './useStakeContext';
import {
selectStakingEligibility,
setStakingEligibility,
} from '../../../../core/redux/slices/staking';

const useStakingEligibility = () => {
const dispatch = useDispatch();
const selectedAddress =
useSelector(selectSelectedInternalAccountChecksummedAddress) || '';
const { isEligible } = useSelector(selectStakingEligibility);

const { stakingApiService } = useStakeContext();

const [isEligible, setIsEligible] = useState<boolean>(false);
const [loading, setLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const fetchStakingEligibility = useCallback(async () => {
try {
setLoading(true);

if (!stakingApiService) {
throw new Error('Staking API service is unavailable');
}

const addresses = selectedAddress ? [selectedAddress] : [];
if (!stakingApiService) {
throw new Error('Staking API service is unavailable');
}

// Directly calling the stakingApiService to fetch staking eligibility
const { eligible } = await stakingApiService.getPooledStakingEligibility(
addresses,
);
setIsLoading(true);
setError(null);

setIsEligible(eligible);
try {
const { eligible } = await stakingApiService.getPooledStakingEligibility([
selectedAddress,
]);
dispatch(setStakingEligibility(eligible));
return { isEligible: eligible };
} catch (err) {
setError('Failed to fetch pooled staking eligibility');
return { isEligible: false };
} finally {
setLoading(false);
setIsLoading(false);
}
}, [selectedAddress, stakingApiService]);
}, [selectedAddress, stakingApiService, dispatch]);

useEffect(() => {
fetchStakingEligibility();
}, [fetchStakingEligibility]);

return {
isEligible,
isLoadingEligibility: loading,
refreshPooledStakingEligibility: fetchStakingEligibility,
isLoadingEligibility: isLoading,
error,
refreshPooledStakingEligibility: fetchStakingEligibility,
};
};

Expand Down
Loading

0 comments on commit 39536b5

Please sign in to comment.