Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PooledStaking slice for managing staking state #12363

Merged
merged 7 commits into from
Nov 26, 2024
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
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]);

amitabh94 marked this conversation as resolved.
Show resolved Hide resolved
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 };
amitabh94 marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading