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: STAKE-822 build your balance component #11261

Merged
merged 48 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a725f41
feat: refactored staking balance after merging main
Matt561 Sep 18, 2024
ad091a4
feat: added related copy to en.json
Matt561 Sep 19, 2024
0533fa6
Merge remote-tracking branch 'origin/main' into feat/stake-822-build-…
Matt561 Sep 19, 2024
ef6a865
feat: moved StakingBalance into ui/stake directory
Matt561 Sep 19, 2024
d797afa
feat: connect stake balance stake button to stake input view
Matt561 Sep 19, 2024
e30ca34
feat: added tests for StakingBalance component
Matt561 Sep 19, 2024
f7887b8
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 19, 2024
8e99a0a
fix: reverted storybook changes from component that has been removed
Matt561 Sep 19, 2024
8117dfa
feat: StakingButtons cleanup
Matt561 Sep 19, 2024
6a1a607
feat: reverted storyLoader.js
Matt561 Sep 19, 2024
48d8922
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 20, 2024
7bd6857
feat: added geo block banner and minor cleanup
Matt561 Sep 20, 2024
5f22b39
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 20, 2024
1453939
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 23, 2024
75a30df
fix: updated tests to match testing guidelines
Matt561 Sep 23, 2024
3a83712
feat: added staking balance API response type
Matt561 Sep 23, 2024
0e89a0a
feat: replaced previous mock data with new mock that matches expect A…
Matt561 Sep 23, 2024
189d72a
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 23, 2024
cdf2bcc
fix: cleanup and added tests for utils
Matt561 Sep 23, 2024
0d1a6f6
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 23, 2024
682756f
fix: filterExitRequests typo
Matt561 Sep 23, 2024
3ce7e7d
fix: broke out filterExistRequests
Matt561 Sep 23, 2024
33209a0
fix: use BN when reducing claimable exit requests
Matt561 Sep 23, 2024
120f58c
fix: added comment to renderUnstakingTimeRemaining helper
Matt561 Sep 23, 2024
a7b463e
feat: added bignumber util from portfolio app
Matt561 Sep 24, 2024
ec147ed
feat: added value utils from portfolio app
Matt561 Sep 24, 2024
e62f289
fix: renamed StakingBalance.styles.tsx to StakingBalance.styles.ts
Matt561 Sep 24, 2024
b1ab055
feat: changes based on PR review comments
Matt561 Sep 24, 2024
a417c95
fix: updated tests
Matt561 Sep 24, 2024
ac0a540
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 24, 2024
e031b95
Merge remote-tracking branch 'origin/main' into feat/stake-822-build-…
Matt561 Sep 24, 2024
ecfa5df
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 24, 2024
0340260
fix: updated date util tests
Matt561 Sep 24, 2024
c609085
fix: fixed bignumber tests
Matt561 Sep 24, 2024
5ad7154
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 24, 2024
b4eb37d
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 24, 2024
34e777a
fix: resolving sonarcloud smells
Matt561 Sep 24, 2024
f60d846
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 24, 2024
dffaebb
fix: moved value utils into staking utils
Matt561 Sep 25, 2024
07bba40
fix: moved bignumber utils into staking utils
Matt561 Sep 25, 2024
dbcf576
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 25, 2024
c67392c
fix: remove default stretching of claim button in unstaking banner
Matt561 Sep 25, 2024
9662cf1
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 25, 2024
36ee743
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 25, 2024
2ffffec
Merge remote-tracking branch 'origin/main' into feat/stake-822-build-…
Matt561 Sep 25, 2024
ba6634a
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 25, 2024
40c870c
Merge branch 'main' into feat/stake-822-build-your-balance-component
Matt561 Sep 25, 2024
fdcd2b1
fix: simplified claimableEth calculation for StakingBalance
Matt561 Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/components/UI/AssetOverview/Balance/Balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import Text, {
} from '../../../../component-library/components/Texts/Text';
import { TokenI } from '../../Tokens/types';
import { useNavigation } from '@react-navigation/native';
import { isPooledStakingFeatureEnabled } from '../../Stake/constants';
import StakingBalance from '../../Stake/components/StakingBalance/StakingBalance';

interface BalanceProps {
asset: TokenI;
mainBalance: string;
Expand Down Expand Up @@ -86,6 +89,7 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => {
{asset.name || asset.symbol}
</Text>
</AssetElement>
{isPooledStakingFeatureEnabled() && asset?.isETH && <StakingBalance />}
</View>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';

const styleSheet = () =>
StyleSheet.create({
container: {
paddingHorizontal: 16,
},
badgeWrapper: {
alignSelf: 'center',
},
balances: {
flex: 1,
justifyContent: 'center',
marginLeft: 20,
alignSelf: 'center',
},
ethLogo: {
width: 32,
height: 32,
borderRadius: 16,
overflow: 'hidden',
},
bannerStyles: {
marginVertical: 8,
},
buttonsContainer: {
paddingTop: 8,
},
});

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
import { renderScreen } from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import StakingBalance from './StakingBalance';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';

function render(Component: React.ComponentType) {
return renderScreen(
Component,
{
name: 'Asset',
},
{
state: {
engine: {
backgroundState,
},
},
},
);
}

const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => {
const actualReactNavigation = jest.requireActual('@react-navigation/native');
return {
...actualReactNavigation,
useNavigation: () => ({
navigate: mockNavigate,
}),
};
});
describe('StakingBalance', () => {
it('render matches snapshot', () => {
render(StakingBalance);
expect(screen.toJSON()).toMatchSnapshot();
});

it('redirects to StakeInputView on stake button click', () => {
render(StakingBalance);

fireEvent.press(screen.getByText(strings('stake.stake_more')));

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(Routes.STAKE.STAKE);
});
});
189 changes: 189 additions & 0 deletions app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import React, { useMemo, useState } from 'react';
import Badge, {
BadgeVariant,
} from '../../../../../component-library/components/Badges/Badge';
import BadgeWrapper from '../../../../../component-library/components/Badges/BadgeWrapper';
import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../component-library/hooks';
import AssetElement from '../../../AssetElement';
import NetworkMainAssetLogo from '../../../NetworkMainAssetLogo';
import { selectNetworkName } from '../../../../../selectors/networkInfos';
import { useSelector } from 'react-redux';
import images from '../../../../../images/image-icons';
import styleSheet from './StakingBalance.styles';
import { View } from 'react-native';
import StakingButtons from './StakingButtons/StakingButtons';
import ClaimBanner from './StakingBanners/ClaimBanner/ClaimBanner';
import UnstakingBanner from './StakingBanners/UnstakeBanner/UnstakeBanner';
import Banner, {
BannerAlertSeverity,
BannerVariant,
} from '../../../../../component-library/components/Banners/Banner';
import { strings } from '../../../../../../locales/i18n';
import { renderFromWei } from '../../../../../util/number';
import { GetStakesApiResponse } from './StakingBalance.types';
import { TokenI } from '../../../../UI/Tokens/types';
import { getTimeDifferenceFromNow } from '../../../../../util/date';
import { filterExitRequests } from './utils';
import { BN } from 'ethereumjs-util';
import bn from 'bignumber.js';
import { fixDisplayAmount } from '../../utils/value';
import { multiplyValueByPowerOfTen } from '../../utils/bignumber';

// TODO: Replace mock data when connecting to backend.
const MOCK_STAKED_ETH_ASSET = {
balance: '4.9999 ETH',
balanceFiat: '$13,292.20',
name: 'Staked Ethereum',
symbol: 'ETH',
} as TokenI;

// TODO: Replace mock data when connecting to backend.
const MOCK_UNSTAKING_REQUESTS: GetStakesApiResponse = {
accounts: [
{
account: '0x0123456789abcdef0123456789abcdef01234567',
lifetimeRewards: '43927049303048',
assets: '17913326707142320',
exitRequests: [
{
// Unstaking
positionTicket: '2153260738145148336740',
timestamp: '1727110415000',
totalShares: '989278156820374',
withdrawalTimestamp: null,
exitQueueIndex: '-1',
claimedAssets: null,
leftShares: null,
},
// Requests below are claimable.
{
positionTicket: '515964521392314631201',
timestamp: '1720539311000',
totalShares: '99473618267007',
withdrawalTimestamp: '0',
exitQueueIndex: '57',
claimedAssets: '100006626507361',
leftShares: '0',
},
{
positionTicket: '515964620865932898208',
timestamp: '1720541495000',
totalShares: '99473618267007',
withdrawalTimestamp: '0',
exitQueueIndex: '57',
claimedAssets: '100006626507361',
leftShares: '0',
},
{
positionTicket: '516604671289934191921',
timestamp: '1720607327000',
totalShares: '1929478758729790',
withdrawalTimestamp: '0',
exitQueueIndex: '58',
claimedAssets: '1939870510970987',
leftShares: '0',
},
],
},
],
exchangeRate: '1.010906701603882254',
};

const StakingBalance = () => {
const { styles } = useStyles(styleSheet, {});

const networkName = useSelector(selectNetworkName);

const [isGeoBlocked] = useState(true);

const { unstakingRequests, claimableRequests } = useMemo(
() =>
filterExitRequests(
MOCK_UNSTAKING_REQUESTS.accounts[0].exitRequests,
MOCK_UNSTAKING_REQUESTS.exchangeRate,
),
[],
);

const claimableEth = useMemo(
() =>
renderFromWei(
claimableRequests.reduce(
(acc, { claimedAssets }) =>
claimedAssets ? acc.add(new BN(claimedAssets)) : acc,
new BN(0),
),
),
[claimableRequests],
);

const hasClaimableEth = !!Number(claimableEth);

return (
<View>
<AssetElement
Matt561 marked this conversation as resolved.
Show resolved Hide resolved
asset={MOCK_STAKED_ETH_ASSET}
mainBalance={MOCK_STAKED_ETH_ASSET.balance}
balance={MOCK_STAKED_ETH_ASSET.balanceFiat}
>
<BadgeWrapper
style={styles.badgeWrapper}
badgeElement={
<Badge
variant={BadgeVariant.Network}
imageSource={images.ETHEREUM}
name={networkName}
/>
}
>
<NetworkMainAssetLogo style={styles.ethLogo} />
</BadgeWrapper>
<Text style={styles.balances} variant={TextVariant.BodyLGMedium}>
{MOCK_STAKED_ETH_ASSET.name || MOCK_STAKED_ETH_ASSET.symbol}
</Text>
</AssetElement>
<View style={styles.container}>
{unstakingRequests.map(
({ positionTicket, withdrawalTimestamp, assetsToDisplay }) =>
assetsToDisplay && (
<UnstakingBanner
key={positionTicket}
Matt561 marked this conversation as resolved.
Show resolved Hide resolved
amountEth={fixDisplayAmount(
multiplyValueByPowerOfTen(new bn(assetsToDisplay), -18),
4,
)}
timeRemaining={
!Number(withdrawalTimestamp)
? { days: 11, hours: 0, minutes: 0 } // default to 11 days.
: getTimeDifferenceFromNow(Number(withdrawalTimestamp))
}
style={styles.bannerStyles}
/>
),
)}
{hasClaimableEth && (
<ClaimBanner
claimableAmount={claimableEth}
style={styles.bannerStyles}
/>
)}
{isGeoBlocked && (
<Banner
variant={BannerVariant.Alert}
severity={BannerAlertSeverity.Warning}
description={strings('stake.banner_text.geo_blocked')}
style={styles.bannerStyles}
/>
)}
<View style={styles.buttonsContainer}>
<StakingButtons />
</View>
</View>
</View>
);
};

export default StakingBalance;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface ExitRequest {
positionTicket: string; // BigInt!
timestamp: string; // BigInt!
totalShares: string; // BigInt!
receiver: string; // Bytes!
/**
* If `withdrawalTimestamp` is null, it means the request hasn't been processed yet.
* If `withdrawalTimestamp` is "0", funds are withdrawable.
* Else, `withdrawalTimestamp` shows an approximate UTC timestamp of when funds will be withdrawable.
* `withdrawalTimestamp` is updated every 6 hours.
*/
withdrawalTimestamp: string | null; // BigInt
}

export type ExitRequestWithClaimedAssetInfo = Pick<
ExitRequest,
'positionTicket' | 'timestamp' | 'totalShares' | 'withdrawalTimestamp'
> & {
exitQueueIndex: string;
claimedAssets: string | null;
leftShares: string | null;
};

interface StakeByAccount {
account: string;
lifetimeRewards: string;
assets: string;
exitRequests: ExitRequestWithClaimedAssetInfo[];
}

export interface GetStakesApiResponse {
accounts: StakeByAccount[];
exchangeRate: string;
}

export interface UnstakingRequest extends ExitRequestWithClaimedAssetInfo {
assetsToDisplay: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StyleSheet } from 'react-native';

const styleSheet = () =>
StyleSheet.create({
claimButton: {
alignSelf: 'flex-start',
},
});

export default styleSheet;
Loading
Loading