From 0be191466b8ab9de8e046ee259f9edf370eee102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Thu, 27 Feb 2025 22:53:30 +0100 Subject: [PATCH 1/2] feat(suite-native): solana staking view only --- .../wallet-utils/src/solanaStakingUtils.ts | 6 + suite-native/intl/src/en.ts | 3 + .../src/components/StakePendingCard.tsx | 45 +++++-- .../StakingBalancesOverviewCard.tsx | 8 +- suite-native/staking/src/selectors.ts | 56 ++++++-- .../staking/src/solanaStakingSelectors.ts | 120 ++++++++++++++++++ suite-native/staking/src/utils.ts | 2 +- 7 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 suite-native/staking/src/solanaStakingSelectors.ts diff --git a/suite-common/wallet-utils/src/solanaStakingUtils.ts b/suite-common/wallet-utils/src/solanaStakingUtils.ts index 5391316e710..b0d4ee542dd 100644 --- a/suite-common/wallet-utils/src/solanaStakingUtils.ts +++ b/suite-common/wallet-utils/src/solanaStakingUtils.ts @@ -82,6 +82,12 @@ export const getSolAccountTotalStakingBalance = (account: Account) => { return formatNetworkAmount(totalStakingBalance, account.symbol); }; +export const getSolanaCryptoBalanceWithStaking = (account: Account) => { + const stakingBalance = getSolAccountTotalStakingBalance(account); + + return new BigNumber(account.formattedBalance).plus(stakingBalance ?? 0).toString(); +}; + export const calculateSolanaStakingReward = (accountBalance?: string, apy?: string) => { if (!accountBalance || !apy) return '0'; diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index 1954daa6c9e..a9e0244fd1b 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1244,12 +1244,15 @@ export const en = { }, staked: 'Staked', rewards: 'Rewards', + rewardsPerEpoch: 'Rewards per Epoch', apy: 'Annual percentage yield', stakingCanBeManaged: 'Staking can be currently managed only in', trezorDesktop: 'Trezor Suite for desktop.', stakePendingCard: { totalStakePending: 'Total stake pending', addingToStakingPool: 'Adding to staking pool', + activatingStake: 'Activating stake', + totalStakeActivating: 'Total stake activating', transactionPending: 'Transaction pending', unknownStatus: 'Unknown status', }, diff --git a/suite-native/module-staking-management/src/components/StakePendingCard.tsx b/suite-native/module-staking-management/src/components/StakePendingCard.tsx index 750745f9631..5d05085dcfd 100644 --- a/suite-native/module-staking-management/src/components/StakePendingCard.tsx +++ b/suite-native/module-staking-management/src/components/StakePendingCard.tsx @@ -3,6 +3,7 @@ import { TouchableOpacity } from 'react-native'; import { useSelector } from 'react-redux'; import { BASE_CRYPTO_MAX_DISPLAYED_DECIMALS } from '@suite-common/formatters'; +import { NetworkSymbol } from '@suite-common/wallet-config'; import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core'; import { Box, Card, Text } from '@suite-native/atoms'; import { CryptoAmountFormatter, CryptoToFiatAmountFormatter } from '@suite-native/formatters'; @@ -15,6 +16,10 @@ import { import { NativeStakingRootState } from '@suite-native/staking/src/types'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +type StakePendingCardProps = { + accountKey: string; + handleToggleBottomSheet: (value: boolean) => void; +}; const stakingItemStyle = prepareNativeStyle(utils => ({ flexDirection: 'row', alignItems: 'center', @@ -22,13 +27,23 @@ const stakingItemStyle = prepareNativeStyle(utils => ({ })); const valuesContainerStyle = prepareNativeStyle(utils => ({ - maxWidth: '40%', + maxWidth: '45%', flexShrink: 0, alignItems: 'flex-end', paddingLeft: utils.spacings.sp8, })); -const getCardAlertProps = (isStakeConfirming: boolean, isStakePending: boolean) => { +const isSolana = (symbol: NetworkSymbol) => ['sol', 'dsol'].includes(symbol); + +const getCardAlertProps = ( + symbol: NetworkSymbol | null, + isStakeConfirming: boolean, + isStakePending: boolean, +) => { + if (!symbol) { + return { alertTitle: undefined, alertVariant: undefined } as const; + } + if (isStakeConfirming && !isStakePending) { return { alertTitle: , @@ -37,7 +52,11 @@ const getCardAlertProps = (isStakeConfirming: boolean, isStakePending: boolean) } if (!isStakeConfirming && isStakePending) { return { - alertTitle: , + alertTitle: isSolana(symbol) ? ( + + ) : ( + + ), alertVariant: 'loading', } as const; } @@ -48,10 +67,12 @@ const getCardAlertProps = (isStakeConfirming: boolean, isStakePending: boolean) } as const; }; -type StakePendingCardProps = { - accountKey: string; - handleToggleBottomSheet: (value: boolean) => void; -}; +const getTitle = (symbol: NetworkSymbol) => + isSolana(symbol) ? ( + + ) : ( + + ); export const StakePendingCard = ({ accountKey, @@ -75,20 +96,20 @@ export const StakePendingCard = ({ ); const cardAlertProps = useMemo( - () => getCardAlertProps(isStakeConfirming, isStakePending), - [isStakeConfirming, isStakePending], + () => getCardAlertProps(symbol, isStakeConfirming, isStakePending), + [symbol, isStakeConfirming, isStakePending], ); if (!symbol || !cardAlertProps.alertVariant) return null; + const title = getTitle(symbol); + return ( handleToggleBottomSheet(true)}> - - - + {title} + ) : ( + + ); + return ( handleToggleBottomSheet(true)}> @@ -99,7 +105,7 @@ export const StakingBalancesOverviewCard = ({ - + {rewardsTitle} { case 'thol': case 'tsep': return getEthereumCryptoBalanceWithStaking(account); + case 'dsol': + case 'sol': + return getSolanaCryptoBalanceWithStaking(account); default: // This is to make sure that all cases are handled. account.symbol satisfies never; @@ -87,6 +106,9 @@ export const selectAccountHasStaking = (state: NativeStakingRootState, accountKe case 'thol': case 'tsep': return selectEthereumAccountHasStaking(state, accountKey); + case 'dsol': + case 'sol': + return selectSolAccountHasStaked(state, accountKey); default: // This throws error if any symbol is not handled. symbol satisfies never; @@ -110,6 +132,9 @@ export const selectIsStakePendingByAccountKey = ( case 'thol': case 'tsep': return selectEthereumIsStakePendingByAccountKey(state, accountKey); + case 'dsol': + case 'sol': + return selectSolanaIsStakePendingByAccountKey(state, accountKey); default: // This throws error if any symbol is not handled. symbol satisfies never; @@ -133,6 +158,9 @@ export const selectIsStakeConfirmingByAccountKey = ( case 'thol': case 'tsep': return selectEthereumIsStakeConfirmingByAccountKey(state, accountKey); + case 'dsol': + case 'sol': + return false; // there are no pending txns for solana staking; default: // This throws error if any symbol is not handled. symbol satisfies never; @@ -148,17 +176,7 @@ export const selectAPYByAccountKey = (state: NativeStakingRootState, accountKey: return null; } - switch (symbol) { - case 'eth': - case 'thol': - case 'tsep': - return selectEthereumAPYByAccountKey(state, accountKey); - default: - // This throws error if any symbol is not handled. - symbol satisfies never; - - return null; - } + return selectPoolStatsApyData(state, symbol); }; export const selectStakedBalanceByAccountKey = ( @@ -176,6 +194,9 @@ export const selectStakedBalanceByAccountKey = ( case 'thol': case 'tsep': return selectEthereumStakedBalanceByAccountKey(state, accountKey); + case 'dsol': + case 'sol': + return selectSolanaStakedBalanceByAccountKey(state, accountKey); default: // This throws error if any symbol is not handled. symbol satisfies never; @@ -199,6 +220,10 @@ export const selectRewardsBalanceByAccountKey = ( case 'thol': case 'tsep': return selectEthereumRewardsBalanceByAccountKey(state, accountKey); + case 'dsol': + case 'sol': + // on solana we show rewards per one epoch + return selectExpectedRewardsForEpoch(state, accountKey); default: // This throws error if any symbol is not handled. symbol satisfies never; @@ -222,6 +247,9 @@ export const selectTotalStakePendingByAccountKey = ( case 'thol': case 'tsep': return selectEthereumTotalStakePendingByAccountKey(state, accountKey); + case 'dsol': + case 'sol': + return selectSolanaTotalStakePendingByAccountKey(state, accountKey); default: // This throws error if any symbol is not handled. symbol satisfies never; diff --git a/suite-native/staking/src/solanaStakingSelectors.ts b/suite-native/staking/src/solanaStakingSelectors.ts new file mode 100644 index 00000000000..83df0210975 --- /dev/null +++ b/suite-native/staking/src/solanaStakingSelectors.ts @@ -0,0 +1,120 @@ +import { createWeakMapSelector } from '@suite-common/redux-utils'; +import type { NetworkSymbol } from '@suite-common/wallet-config'; +import { + AccountsRootState, + StakeRootState, + selectAccountByKey, + selectAccountNetworkSymbol, + selectDeviceAccounts, + selectPoolStatsApyData, +} from '@suite-common/wallet-core'; +import { + calculateSolanaStakingReward, + getSolStakingAccountsInfo, +} from '@suite-common/wallet-utils'; +import { BigNumber } from '@trezor/utils'; + +import { NativeStakingRootState } from './types'; + +export const createMemoizedSelector = createWeakMapSelector.withTypes(); + +export const selectVisibleDeviceSolanaAccountsWithStakingByNetworkSymbol = ( + state: NativeStakingRootState, + symbol: NetworkSymbol | null, +) => { + const accounts = selectDeviceAccounts(state); + + return accounts.filter( + account => + account.symbol === symbol && + account.visible && + account?.misc && + account.networkType === 'solana' && + !!account.misc.solStakingAccounts?.length, + ); +}; + +const selectSolStakingAccountsInfoByAccountKey = createMemoizedSelector( + [selectAccountByKey], + account => { + if (!account) { + return null; + } + + return getSolStakingAccountsInfo(account); + }, +); + +export const selectSolanaIsStakePendingByAccountKey = ( + state: AccountsRootState, + accountKey: string, +) => { + const stakingInfo = selectSolStakingAccountsInfoByAccountKey(state, accountKey); + + if (!stakingInfo) { + return false; + } + + const isStakePending = + Number(stakingInfo?.solPendingStakeBalance ?? 0) + + Number(stakingInfo?.solPendingUnstakeBalance ?? 0) > + 0; + + return isStakePending; +}; + +export const selectSolanaAPYByAccountKey = ( + state: StakeRootState & AccountsRootState, + accountKey: string, +) => { + const symbol = selectAccountNetworkSymbol(state, accountKey); + if (!symbol) return 0; + + return selectPoolStatsApyData(state, symbol); +}; + +export const selectSolanaStakedBalanceByAccountKey = ( + state: AccountsRootState, + accountKey: string, +) => { + const stakingInfo = selectSolStakingAccountsInfoByAccountKey(state, accountKey); + if (!stakingInfo) { + return '0'; + } + + return new BigNumber(stakingInfo.solStakedBalance) + .plus(stakingInfo.solClaimableBalance) + .plus(stakingInfo.solPendingStakeBalance) + .plus(stakingInfo.solPendingUnstakeBalance) + .toString(); +}; + +export const selectExpectedRewardsForEpoch = ( + state: StakeRootState & AccountsRootState, + accountKey: string, +) => { + const stakingInfo = selectSolStakingAccountsInfoByAccountKey(state, accountKey); + const apy = selectSolanaAPYByAccountKey(state, accountKey).toString(); + + if (!stakingInfo) { + return '0'; + } + + const yieldBearingBalance = new BigNumber(stakingInfo.solStakedBalance) + .plus(stakingInfo.solPendingUnstakeBalance) + .toString(); + + return calculateSolanaStakingReward(yieldBearingBalance, apy); +}; + +export const selectSolanaTotalStakePendingByAccountKey = ( + state: AccountsRootState, + accountKey: string, +) => { + const stakingInfo = selectSolStakingAccountsInfoByAccountKey(state, accountKey); + if (!stakingInfo) { + return '0'; + } + + return stakingInfo.solPendingStakeBalance; +}; diff --git a/suite-native/staking/src/utils.ts b/suite-native/staking/src/utils.ts index c4d37c0e986..f4ae34fbc3c 100644 --- a/suite-native/staking/src/utils.ts +++ b/suite-native/staking/src/utils.ts @@ -1,7 +1,7 @@ import { NetworkSymbol } from '@suite-common/wallet-config'; import { isArrayMember } from '@trezor/utils'; -const stakingCoins = ['eth', 'thol', 'tsep'] as const satisfies NetworkSymbol[]; +const stakingCoins = ['eth', 'thol', 'tsep', 'sol', 'dsol'] as const satisfies NetworkSymbol[]; type NetworkSymbolWithStaking = (typeof stakingCoins)[number]; export const doesCoinSupportStaking = (symbol: NetworkSymbol): symbol is NetworkSymbolWithStaking => From 820c91392023ada442f4e0739b5f82e35aa45d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Fri, 28 Feb 2025 17:05:45 +0100 Subject: [PATCH 2/2] fixup! feat(suite-native): solana staking view only --- suite-native/staking/src/solanaStakingSelectors.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/suite-native/staking/src/solanaStakingSelectors.ts b/suite-native/staking/src/solanaStakingSelectors.ts index 83df0210975..5821bbb3a8c 100644 --- a/suite-native/staking/src/solanaStakingSelectors.ts +++ b/suite-native/staking/src/solanaStakingSelectors.ts @@ -83,8 +83,6 @@ export const selectSolanaStakedBalanceByAccountKey = ( } return new BigNumber(stakingInfo.solStakedBalance) - .plus(stakingInfo.solClaimableBalance) - .plus(stakingInfo.solPendingStakeBalance) .plus(stakingInfo.solPendingUnstakeBalance) .toString(); };