From f059e14e8fe560471858f71b7976c8e9fceaf851 Mon Sep 17 00:00:00 2001
From: Pavlo Syrotyna
Date: Wed, 18 Dec 2024 16:26:39 +0200
Subject: [PATCH] feat(suite): implement solana staking dashboard
---
packages/suite-desktop-core/src/config.ts | 1 +
.../components/suite/CoinList/CoinList.tsx | 20 ++-
.../suite/StakingProcess/StakingInfo.tsx | 67 ++++++--
.../suite/StakingProcess/UnstakingInfo.tsx | 54 +++++-
.../PageNames/AccountName/AccountDetails.tsx | 3 +-
.../AdvancedCoinSettingsModal.tsx | 6 +-
.../ClaimModal/ClaimModal.tsx | 4 +-
.../StakeModal/StakeEthForm/Inputs.tsx | 2 +-
.../StakingInfoCards/EstimatedGains.tsx | 24 ++-
.../UnstakeModal/UnstakeEthForm/Options.tsx | 4 +-
.../UnstakeEthForm/UnstakeEthForm.tsx | 4 +-
.../AccountBanners/StakeEthBanner.tsx | 4 +-
.../AccountsMenu/AccountSection.tsx | 8 +-
.../src/hooks/wallet/useUnstakeEthForm.ts | 4 +-
packages/suite/src/support/messages.ts | 24 +++
.../suite/src/utils/suite/ethereumStaking.ts | 23 ---
.../suite/src/utils/suite/getCoinLabel.ts | 10 +-
packages/suite/src/utils/suite/staking.ts | 24 +++
.../dashboard/StakeEthCard/StakeEthCard.tsx | 6 +-
.../components/EthStakingDashboard.tsx | 25 +--
.../hooks/useProgressLabelsData.tsx | 78 ---------
.../SolStakingDashboard.tsx | 65 ++++++-
.../StakingDashboard/StakingDashboard.tsx | 15 +-
.../components/ApyCard.tsx | 0
.../components/ClaimCard.tsx | 4 +-
.../components/EmptyStakingCard.tsx | 7 +-
.../components/PayoutCard.tsx | 5 +-
.../ProgressLabels/ProgressLabel.tsx | 0
.../ProgressLabels/ProgressLabels.tsx | 0
.../components/ProgressLabels/types.ts | 0
.../components/StakingCard.tsx | 45 +++--
.../components/styled.ts | 0
.../hooks/useIsTxStatusShown.ts | 0
.../hooks/useProgressLabelsData.tsx | 159 ++++++++++++++++++
packages/urls/src/urls.ts | 2 +
.../src/solanaStakingConstants.ts | 10 +-
.../wallet-constants/src/stakingConstants.ts | 2 +
.../src/transactions/transactionsReducer.ts | 4 +-
.../wallet-utils/src/solanaStakingUtils.ts | 126 +++++++++++++-
39 files changed, 627 insertions(+), 212 deletions(-)
create mode 100644 packages/suite/src/utils/suite/staking.ts
delete mode 100644 packages/suite/src/views/wallet/staking/components/EthStakingDashboard/hooks/useProgressLabelsData.tsx
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/ApyCard.tsx (100%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/ClaimCard.tsx (96%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/PayoutCard.tsx (93%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/ProgressLabels/ProgressLabel.tsx (100%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/ProgressLabels/ProgressLabels.tsx (100%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/ProgressLabels/types.ts (100%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/StakingCard.tsx (86%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/components/styled.ts (100%)
rename packages/suite/src/views/wallet/staking/components/{EthStakingDashboard => StakingDashboard}/hooks/useIsTxStatusShown.ts (100%)
create mode 100644 packages/suite/src/views/wallet/staking/components/StakingDashboard/hooks/useProgressLabelsData.tsx
diff --git a/packages/suite-desktop-core/src/config.ts b/packages/suite-desktop-core/src/config.ts
index 7c6d4c7f186..e8b2f2937db 100644
--- a/packages/suite-desktop-core/src/config.ts
+++ b/packages/suite-desktop-core/src/config.ts
@@ -27,6 +27,7 @@ export const allowedDomains = [
'blockfrost.dev',
'eth-api-b2c-stage.everstake.one', // staking endpoint for Holesky testnet, works only with VPN
'eth-api-b2c.everstake.one', // staking endpoint for Ethereum mainnet
+ 'dashboard-api.everstake.one', // staking enpoint for Solana
];
export const cspRules = [
diff --git a/packages/suite/src/components/suite/CoinList/CoinList.tsx b/packages/suite/src/components/suite/CoinList/CoinList.tsx
index 4efa7a15d97..3813ab66946 100644
--- a/packages/suite/src/components/suite/CoinList/CoinList.tsx
+++ b/packages/suite/src/components/suite/CoinList/CoinList.tsx
@@ -9,6 +9,7 @@ import { Network, NetworkSymbol } from '@suite-common/wallet-config';
import { Translation } from 'src/components/suite';
import { useDevice, useDiscovery, useSelector } from 'src/hooks/suite';
import { getCoinLabel } from 'src/utils/suite/getCoinLabel';
+import { selectIsDebugModeActive } from 'src/reducers/suite/suiteReducer';
import { Coin } from './Coin';
@@ -35,6 +36,8 @@ export const CoinList = ({
onToggle,
}: CoinListProps) => {
const { device, isLocked } = useDevice();
+ const isDebug = useSelector(selectIsDebugModeActive);
+
const blockchain = useSelector(state => state.wallet.blockchain);
const isDeviceLocked = !!device && isLocked();
const { isDiscoveryRunning } = useDiscovery();
@@ -50,7 +53,14 @@ export const CoinList = ({
return (
{networks.map(network => {
- const { symbol, name, support, features, testnet: isTestnet } = network;
+ const {
+ symbol,
+ name,
+ support,
+ features,
+ testnet: isTestnet,
+ networkType,
+ } = network;
const hasCustomBackend = !!blockchain[symbol].backends.selected;
const firmwareSupportRestriction =
@@ -76,7 +86,13 @@ export const CoinList = ({
getCoinUnavailabilityMessage(unavailableReason);
const tooltipString = discoveryTooltip || lockedTooltip || unavailabilityTooltip;
- const label = getCoinLabel(features, isTestnet, hasCustomBackend);
+ const label = getCoinLabel(
+ features,
+ isTestnet,
+ hasCustomBackend,
+ networkType,
+ isDebug,
+ );
return (
{
+ switch (networkType) {
+ case 'ethereum':
+ return {
+ payoutDays: daysToAddToPool,
+ rewardsPeriodHeading: ,
+ rewardsPeriodSubheading: (
+
+ ),
+ rewardsEarningHeading: ,
+ };
+ case 'solana':
+ return {
+ payoutDays: SOLANA_EPOCH_DAYS,
+ rewardsPeriodHeading: ,
+ rewardsPeriodSubheading: ,
+ rewardsEarningHeading: (
+
+ ),
+ };
+ default:
+ return null;
+ }
+};
+
interface StakingInfoProps {
isExpanded?: boolean;
}
@@ -33,13 +76,14 @@ export const StakingInfo = ({ isExpanded }: StakingInfoProps) => {
selectAccountStakeTransactions(state, account?.key ?? ''),
);
- const ethApy = useSelector((state: StakeRootState) =>
+ const apy = useSelector((state: StakeRootState) =>
selectPoolStatsApyData(state, account?.symbol),
);
if (!account) return null;
const daysToAddToPool = getDaysToAddToPool(stakeTxs, data);
+ const infoRowsData = getInfoRowsData(account.networkType, account.symbol, daysToAddToPool);
const infoRows = [
{
@@ -47,25 +91,24 @@ export const StakingInfo = ({ isExpanded }: StakingInfoProps) => {
content: { text: , isBadge: true },
},
{
- heading: ,
- subheading: (
-
- ),
+ heading: infoRowsData?.rewardsPeriodHeading,
+ subheading: infoRowsData?.rewardsPeriodSubheading,
content: {
text: (
<>
- ~
+ ~
+
>
),
},
},
{
- heading: ,
+ heading: infoRowsData?.rewardsEarningHeading,
subheading: ,
- content: { text: `~${ethApy}% p.a.` },
+ content: { text: `~${apy}% p.a.` },
},
];
diff --git a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx
index ae28b634b7b..8eed1076d36 100644
--- a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx
+++ b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx
@@ -10,7 +10,8 @@ import {
StakeRootState,
AccountsRootState,
} from '@suite-common/wallet-core';
-import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
+import { getNetworkDisplaySymbol, NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
+import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
import { Translation } from 'src/components/suite';
import { getDaysToUnstake } from 'src/utils/suite/ethereumStaking';
@@ -18,6 +19,40 @@ import { CoinjoinRootState } from 'src/reducers/wallet/coinjoinReducer';
import { InfoRow } from './InfoRow';
+type InfoRowsData = {
+ readyForClaimDays: number | undefined;
+ deactivatePeriodHeading: JSX.Element;
+ deactivatePeriodSubheading: JSX.Element;
+};
+
+const getInfoRowsData = (
+ networkType: NetworkType,
+ accountSymbol: NetworkSymbol,
+ daysToUnstake?: number,
+): InfoRowsData | null => {
+ switch (networkType) {
+ case 'ethereum':
+ return {
+ readyForClaimDays: daysToUnstake,
+ deactivatePeriodHeading: ,
+ deactivatePeriodSubheading: (
+
+ ),
+ };
+ case 'solana':
+ return {
+ readyForClaimDays: SOLANA_EPOCH_DAYS,
+ deactivatePeriodHeading: ,
+ deactivatePeriodSubheading: ,
+ };
+ default:
+ return null;
+ }
+};
+
interface UnstakingInfoProps {
isExpanded?: boolean;
}
@@ -36,6 +71,7 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {
const daysToUnstake = getDaysToUnstake(unstakeTxs, data);
const displaySymbol = getNetworkDisplaySymbol(account.symbol);
+ const infoRowsData = getInfoRowsData(account.networkType, account.symbol, daysToUnstake);
const infoRows = [
{
@@ -46,15 +82,15 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {
},
},
{
- heading: ,
- subheading: (
-
- ),
+ heading: infoRowsData?.deactivatePeriodHeading,
+ subheading: infoRowsData?.deactivatePeriodSubheading,
content: {
- text: ,
+ text: (
+
+ ),
},
},
{
diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/PageHeader/PageNames/AccountName/AccountDetails.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/PageHeader/PageNames/AccountName/AccountDetails.tsx
index f2b44601429..ca39c7d0e2e 100644
--- a/packages/suite/src/components/suite/layouts/SuiteLayout/PageHeader/PageNames/AccountName/AccountDetails.tsx
+++ b/packages/suite/src/components/suite/layouts/SuiteLayout/PageHeader/PageNames/AccountName/AccountDetails.tsx
@@ -6,6 +6,7 @@ import { Account } from '@suite-common/wallet-types';
import { spacingsPx, zIndices, typography } from '@trezor/theme';
import { H2 } from '@trezor/components';
import { CoinLogo } from '@trezor/product-components';
+import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
import {
MetadataLabeling,
@@ -117,7 +118,7 @@ export const AccountDetails = ({ selectedAccount, isBalanceShown }: AccountDetai
showAccountTypeBadge
accountLabel={selectedAccountLabels.accountLabel}
accountType={accountType}
- symbol={selectedAccount.symbol}
+ symbol={getNetworkDisplaySymbol(selectedAccount.symbol)}
index={index}
path={path}
networkType={selectedAccount.networkType}
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AdvancedCoinSettingsModal/AdvancedCoinSettingsModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AdvancedCoinSettingsModal/AdvancedCoinSettingsModal.tsx
index 338d3de364c..19a1ab47bfb 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AdvancedCoinSettingsModal/AdvancedCoinSettingsModal.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AdvancedCoinSettingsModal/AdvancedCoinSettingsModal.tsx
@@ -7,6 +7,7 @@ import { CoinLogo } from '@trezor/product-components';
import { Modal, Translation } from 'src/components/suite';
import { getCoinLabel } from 'src/utils/suite/getCoinLabel';
import { useSelector } from 'src/hooks/suite';
+import { selectIsDebugModeActive } from 'src/reducers/suite/suiteReducer';
import { CustomBackends } from './CustomBackends/CustomBackends';
@@ -42,12 +43,13 @@ interface AdvancedCoinSettingsModalProps {
}
export const AdvancedCoinSettingsModal = ({ symbol, onCancel }: AdvancedCoinSettingsModalProps) => {
+ const isDebug = useSelector(selectIsDebugModeActive);
const blockchain = useSelector(state => state.wallet.blockchain);
const network = getNetwork(symbol);
- const { name, features, testnet: isTestnet } = network;
+ const { name, networkType, features, testnet: isTestnet } = network;
const hasCustomBackend = !!blockchain[symbol].backends.selected;
- const label = getCoinLabel(features, isTestnet, hasCustomBackend);
+ const label = getCoinLabel(features, isTestnet, hasCustomBackend, networkType, isDebug);
return (
{
// used instead of formState.isValid, which is sometimes returning false even if there are no errors
const formIsValid = Object.keys(errors).length === 0;
- const { claimableAmount = '0' } = getAccountEverstakeStakingPool(selectedAccount.account) ?? {};
+ const { claimableAmount = '0' } = getStakingDataForNetwork(selectedAccount.account) ?? {};
const isDisabled =
!(formIsValid && hasValues) || isSubmitting || isLocked() || !device?.available;
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx
index 1a779b02531..b6ff0011572 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx
@@ -15,7 +15,7 @@ import {
validateReserveOrBalance,
} from 'src/utils/suite/validation';
import { FIAT_INPUT, CRYPTO_INPUT } from 'src/types/wallet/stakeForms';
-import { validateStakingMax } from 'src/utils/suite/ethereumStaking';
+import { validateStakingMax } from 'src/utils/suite/staking';
import { FormFractionButtons } from 'src/components/suite/FormFractionButtons';
export const Inputs = () => {
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx
index e172a8371ca..fbaa0b2c14d 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx
@@ -4,13 +4,13 @@ import { useSelector } from 'react-redux';
import { selectPoolStatsApyData, StakeRootState } from '@suite-common/wallet-core';
import { Column, Grid, Image, Paragraph, Text } from '@trezor/components';
import { negativeSpacings, spacings } from '@trezor/theme';
-import { HELP_CENTER_ETH_STAKING } from '@trezor/urls';
+import { HELP_CENTER_ETH_STAKING, HELP_CENTER_SOL_STAKING } from '@trezor/urls';
import { Translation } from 'src/components/suite/Translation';
import { useStakeEthFormContext } from 'src/hooks/wallet/useStakeEthForm';
import { CRYPTO_INPUT } from 'src/types/wallet/stakeForms';
import { FiatValue, FormattedCryptoAmount, TrezorLink } from 'src/components/suite';
-import { calculateGains } from 'src/utils/suite/ethereumStaking';
+import { calculateGains } from 'src/utils/suite/staking';
export const EstimatedGains = () => {
const { account, getValues, formState } = useStakeEthFormContext();
@@ -22,22 +22,22 @@ export const EstimatedGains = () => {
const cryptoInput = hasInvalidFormState || !value ? '0' : value;
- const ethApy = useSelector((state: StakeRootState) =>
+ const apy = useSelector((state: StakeRootState) =>
selectPoolStatsApyData(state, account?.symbol),
);
const gains = [
{
label: ,
- value: calculateGains(cryptoInput, ethApy, 52),
+ value: calculateGains(cryptoInput, apy, 52),
},
{
label: ,
- value: calculateGains(cryptoInput, ethApy, 12),
+ value: calculateGains(cryptoInput, apy, 12),
},
{
label: ,
- value: calculateGains(cryptoInput, ethApy, 1),
+ value: calculateGains(cryptoInput, apy, 1),
},
];
@@ -45,7 +45,7 @@ export const EstimatedGains = () => {
- {ethApy}%
+ {apy}%
{
id="TR_STAKING_YOUR_EARNINGS"
values={{
a: chunks => (
- {chunks}
+
+ {chunks}
+
),
}}
/>
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/Options.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/Options.tsx
index 821c91748a2..f6ebca7d644 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/Options.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/Options.tsx
@@ -6,7 +6,7 @@ import { Radio, Column, Row, Text } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { NetworkSymbol } from '@suite-common/wallet-config';
import { BigNumber } from '@trezor/utils/src/bigNumber';
-import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils';
+import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
import { FiatValue, FormattedCryptoAmount, Translation } from 'src/components/suite';
import { useSelector } from 'src/hooks/suite';
@@ -70,7 +70,7 @@ export const Options = ({ symbol }: OptionsProps) => {
autocompoundBalance = '0',
depositedBalance = '0',
restakedReward = '0',
- } = getAccountEverstakeStakingPool(selectedAccount) ?? {};
+ } = getStakingDataForNetwork(selectedAccount) ?? {};
return (
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx
index d97d303a610..6a625a340b1 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx
@@ -1,8 +1,8 @@
import { InfoItem, Tooltip, Banner, Column, Card } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { selectValidatorsQueueData } from '@suite-common/wallet-core';
-import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils';
import { BigNumber } from '@trezor/utils/src/bigNumber';
+import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
import { Translation } from 'src/components/suite';
import { useSelector } from 'src/hooks/suite';
@@ -40,7 +40,7 @@ export const UnstakeEthForm = () => {
);
const unstakingPeriod = getUnstakingPeriodInDays(validatorWithdrawTime);
const { canClaim = false, claimableAmount = '0' } =
- getAccountEverstakeStakingPool(selectedAccount) ?? {};
+ getStakingDataForNetwork(selectedAccount) ?? {};
const inputError = errors[CRYPTO_INPUT] || errors[FIAT_INPUT];
const showError = inputError && inputError.type === 'compose';
diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx
index 0f51f767252..b5727033621 100644
--- a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx
+++ b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx
@@ -23,7 +23,7 @@ export const StakeEthBanner = ({ account }: StakeEthBannerProps) => {
const dispatch = useDispatch();
const { stakeEthBannerClosed } = useSelector(selectSuiteFlags);
const { route } = useSelector(state => state.router);
- const ethApy = useSelector(state => selectPoolStatsApyData(state, account.symbol));
+ const apy = useSelector(state => selectPoolStatsApyData(state, account.symbol));
const theme = useTheme();
const closeBanner = () => {
@@ -65,7 +65,7 @@ export const StakeEthBanner = ({ account }: StakeEthBannerProps) => {
selectCoinDefinitions(state, symbol));
- const hasStaked = useSelector(state => selectAccountHasStaked(state, account.key));
+ const hasEthStaked = useSelector(state => selectEthAccountHasStaked(state, account.key));
const solStakingAccounts = useSelector(state => selectSolStakingAccounts(state, account.key));
// TODO: remove isDebugModeActive when staking will be ready for launch
- const hasStakingAccount = !!solStakingAccounts?.length && isDebugModeActive; // for solana
+ const hasSolStakingAccount = !!solStakingAccounts?.length && isDebugModeActive; // for solana
const isStakeShown =
- isSupportedStakingNetworkSymbol(symbol) && (hasStaked || hasStakingAccount);
+ isSupportedStakingNetworkSymbol(symbol) && (hasEthStaked || hasSolStakingAccount);
const showGroup = ['ethereum', 'solana', 'cardano'].includes(networkType);
diff --git a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts
index 94ea4298d29..f6c7270a226 100644
--- a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts
+++ b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts
@@ -5,9 +5,9 @@ import useDebounce from 'react-use/lib/useDebounce';
import {
fromFiatCurrency,
- getAccountAutocompoundBalance,
getFeeLevels,
getFiatRateKey,
+ getStakingDataForNetwork,
toFiatCurrency,
} from '@suite-common/wallet-utils';
import { PrecomposedTransactionFinal } from '@suite-common/wallet-types';
@@ -70,7 +70,7 @@ export const useUnstakeEthForm = ({
selectFiatRatesByFiatRateKey(state, getFiatRateKey(symbol, localCurrency), 'current'),
);
- const autocompoundBalance = getAccountAutocompoundBalance(account);
+ const { autocompoundBalance = '0' } = getStakingDataForNetwork(account) ?? {};
const amountLimits: AmountLimitProps = {
currency: symbol,
maxCrypto: autocompoundBalance,
diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts
index 7ca90b1187b..fc4ff6bfc8e 100644
--- a/packages/suite/src/support/messages.ts
+++ b/packages/suite/src/support/messages.ts
@@ -8746,6 +8746,26 @@ export default defineMessages({
id: 'TR_STAKE_ENTER_THE_STAKING_POOL',
defaultMessage: 'Enter the staking pool',
},
+ TR_STAKE_WAIT_FOR_ACTIVATION: {
+ id: 'TR_STAKE_WAIT_FOR_ACTIVATION',
+ defaultMessage: 'Wait for the next epoch until your stake activated',
+ },
+ TR_STAKE_WARM_UP_PERIOD: {
+ id: 'TR_STAKE_WARM_UP_PERIOD',
+ defaultMessage: 'Warm Up period',
+ },
+ TR_STAKE_EARN_REWARDS_EVERY: {
+ id: 'TR_STAKE_EARN_REWARDS_EVERY',
+ defaultMessage: 'Earn rewards every ~{days} days',
+ },
+ TR_STAKE_COOL_DOWN_PERIOD: {
+ id: 'TR_STAKE_COOL_DOWN_PERIOD',
+ defaultMessage: 'Cool Down Period',
+ },
+ TR_STAKE_WAIT_FOR_DEACTIVATION: {
+ id: 'TR_STAKE_WAIT_FOR_DEACTIVATION',
+ defaultMessage: 'Wait for the next epoch until your stake deactivated',
+ },
TR_STAKE_EARN_REWARDS_WEEKLY: {
id: 'TR_STAKE_EARN_REWARDS_WEEKLY',
defaultMessage: 'Earn rewards weekly',
@@ -8900,6 +8920,10 @@ export default defineMessages({
id: 'TR_STAKE_REWARDS',
defaultMessage: 'Rewards',
},
+ TR_STAKE_EXPECTED_REWARDS: {
+ id: 'TR_STAKE_EXPECTED_REWARDS',
+ defaultMessage: 'Expected rewards per 1 epoch (~{days} days)',
+ },
TR_TX_CONFIRMED: {
id: 'TR_TX_CONFIRMED',
defaultMessage: 'Transaction confirmed',
diff --git a/packages/suite/src/utils/suite/ethereumStaking.ts b/packages/suite/src/utils/suite/ethereumStaking.ts
index 3b91937d955..7799398a83d 100644
--- a/packages/suite/src/utils/suite/ethereumStaking.ts
+++ b/packages/suite/src/utils/suite/ethereumStaking.ts
@@ -28,8 +28,6 @@ import { ValidatorsQueue } from '@suite-common/wallet-core';
import { BlockchainEstimatedFee } from '@trezor/connect/src/types/api/blockchainEstimateFee';
import { PartialRecord } from '@trezor/type-utils';
-import { TranslationFunction } from 'src/hooks/suite/useTranslation';
-
const secondsToDays = (seconds: number) => Math.round(seconds / 60 / 60 / 24);
type EthNetwork = 'holesky' | 'mainnet';
@@ -630,27 +628,6 @@ export const getChangedInternalTx = (
return internalTransfer ?? null;
};
-export const calculateGains = (input: string, apy: number, divisor: number) => {
- const amount = new BigNumber(input).multipliedBy(apy).dividedBy(100).dividedBy(divisor);
-
- return amount.toFixed(5, 1);
-};
-
-interface ValidateMaxOptions {
- maxAmount: BigNumber;
- except?: boolean;
-}
-
-export const validateStakingMax =
- (translationString: TranslationFunction, { except, maxAmount }: ValidateMaxOptions) =>
- (value: string) => {
- if (!except && value && BigNumber(value).gt(maxAmount)) {
- return translationString('AMOUNT_EXCEEDS_MAX', {
- maxAmount: maxAmount.toString(),
- });
- }
- };
-
export const simulateUnstake = async ({
amount,
from,
diff --git a/packages/suite/src/utils/suite/getCoinLabel.ts b/packages/suite/src/utils/suite/getCoinLabel.ts
index 66623ee5865..47a65fd6853 100644
--- a/packages/suite/src/utils/suite/getCoinLabel.ts
+++ b/packages/suite/src/utils/suite/getCoinLabel.ts
@@ -1,13 +1,19 @@
import type { TranslationKey } from '@suite-common/intl-types';
-import type { NetworkFeature } from '@suite-common/wallet-config';
+import type { NetworkFeature, NetworkType } from '@suite-common/wallet-config';
export const getCoinLabel = (
features: NetworkFeature[],
isTestnet: boolean,
isCustomBackend: boolean,
+ networkType?: NetworkType,
+ isDebug?: boolean,
): TranslationKey | undefined => {
const hasTokens = features.includes('tokens');
- const hasStaking = features.includes('staking');
+ // TODO: Remove this condition when Solana staking is available
+ const hasStaking =
+ networkType === 'solana'
+ ? isDebug && features.includes('staking') // Solana staking only in debug mode
+ : features.includes('staking');
if (isCustomBackend) {
return 'TR_CUSTOM_BACKEND';
diff --git a/packages/suite/src/utils/suite/staking.ts b/packages/suite/src/utils/suite/staking.ts
new file mode 100644
index 00000000000..dd32d8660a4
--- /dev/null
+++ b/packages/suite/src/utils/suite/staking.ts
@@ -0,0 +1,24 @@
+import { BigNumber } from '@trezor/utils';
+
+import { TranslationFunction } from 'src/hooks/suite/useTranslation';
+
+interface ValidateMaxOptions {
+ maxAmount: BigNumber;
+ except?: boolean;
+}
+
+export const validateStakingMax =
+ (translationString: TranslationFunction, { except, maxAmount }: ValidateMaxOptions) =>
+ (value: string) => {
+ if (!except && value && BigNumber(value).gt(maxAmount)) {
+ return translationString('AMOUNT_EXCEEDS_MAX', {
+ maxAmount: maxAmount.toString(),
+ });
+ }
+ };
+
+export const calculateGains = (input: string, apy: number, divisor: number) => {
+ const amount = new BigNumber(input).multipliedBy(apy).dividedBy(100).dividedBy(divisor);
+
+ return amount.toFixed(5, 1);
+};
diff --git a/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx b/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx
index ced38e360bb..fa8c861b273 100644
--- a/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx
+++ b/packages/suite/src/views/dashboard/StakeEthCard/StakeEthCard.tsx
@@ -46,7 +46,7 @@ export const StakeEthCard = () => {
showDashboardStakingPromoBanner &&
!isBitcoinOnlyDevice;
- const ethApy = useSelector(state => selectPoolStatsApyData(state, bannerSymbol));
+ const apy = useSelector(state => selectPoolStatsApyData(state, bannerSymbol));
const { discovery } = useDiscovery();
const { accounts } = useAccounts(discovery);
@@ -72,7 +72,7 @@ export const StakeEthCard = () => {
(
{
description: ,
},
],
- [ethApy],
+ [apy],
);
if (!isShown) return null;
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx
index ace64bc2718..6076c420c33 100644
--- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/EthStakingDashboard.tsx
@@ -10,24 +10,25 @@ import {
selectPoolStatsNextRewardPayout,
selectValidatorsQueue,
} from '@suite-common/wallet-core';
-import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils';
-import { SelectedAccountStatus } from '@suite-common/wallet-types';
+import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
+import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
+import { SelectedAccountLoaded } from '@suite-common/wallet-types';
import { useDispatch, useSelector } from 'src/hooks/suite';
import { Translation } from 'src/components/suite';
import { DashboardSection } from 'src/components/dashboard';
import { getDaysToAddToPool, getDaysToUnstake } from 'src/utils/suite/ethereumStaking';
-import { StakingCard } from './StakingCard';
-import { ApyCard } from './ApyCard';
-import { PayoutCard } from './PayoutCard';
-import { ClaimCard } from './ClaimCard';
+import { StakingCard } from '../../StakingDashboard/components/StakingCard';
+import { ClaimCard } from '../../StakingDashboard/components/ClaimCard';
+import { StakingDashboard } from '../../StakingDashboard/StakingDashboard';
+import { ApyCard } from '../../StakingDashboard/components/ApyCard';
+import { PayoutCard } from '../../StakingDashboard/components/PayoutCard';
import { Transactions } from './Transactions';
import { InstantStakeBanner } from './InstantStakeBanner';
-import { StakingDashboard } from '../../StakingDashboard/StakingDashboard';
interface EthStakingDashboardProps {
- selectedAccount: SelectedAccountStatus;
+ selectedAccount: SelectedAccountLoaded;
}
export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProps) => {
@@ -38,7 +39,7 @@ export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProp
const { data, isLoading } =
useSelector(state => selectValidatorsQueue(state, account?.symbol)) || {};
- const ethApy = useSelector(state => selectPoolStatsApyData(state, account?.symbol));
+ const apy = useSelector(state => selectPoolStatsApyData(state, account?.symbol));
const nextRewardPayout = useSelector(state =>
selectPoolStatsNextRewardPayout(state, account?.symbol),
);
@@ -64,7 +65,7 @@ export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProp
const daysToAddToPool = getDaysToAddToPool(stakeTxs, data);
const daysToUnstake = getDaysToUnstake(unstakeTxs, data);
- const { canClaim = false } = getAccountEverstakeStakingPool(account) ?? {};
+ const { canClaim = false } = getStakingDataForNetwork(account) ?? {};
return (
}
>
@@ -88,7 +89,7 @@ export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProp
-
+
{
- const progressLabelsData: ProgressLabelData[] = useMemo(
- () => [
- {
- id: 0,
- progressState: (() => {
- if (isStakeConfirming) return 'active';
-
- return 'done';
- })(),
- children: isStakeConfirming ? (
-
- ) : (
-
- ),
- },
- {
- id: 1,
- progressState: (() => {
- if (!isStakeConfirming && isStakePending) return 'active';
- if (!isStakeConfirming && !isStakePending) return 'done';
-
- return 'stale';
- })(),
- children: (
-
-
- {isDaysToAddToPoolShown && (
-
- ~
-
-
- )}
-
- ),
- },
- {
- id: 2,
- progressState: (() => {
- if (!isStakeConfirming && !isStakePending) {
- return 'active';
- }
-
- return 'stale';
- })(),
- children: ,
- },
- ],
- [daysToAddToPool, isDaysToAddToPoolShown, isStakeConfirming, isStakePending],
- );
-
- return { progressLabelsData };
-};
diff --git a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx
index 98db57dae5b..f2c41e5a615 100644
--- a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx
@@ -1,17 +1,74 @@
-import { SelectedAccountStatus } from '@suite-common/wallet-types';
+import { useSelector } from 'react-redux';
+
+import { Column, Flex, Grid, useMediaQuery, variables } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+import { SelectedAccountLoaded } from '@suite-common/wallet-types';
+import { selectPoolStatsApyData, StakeRootState } from '@suite-common/wallet-core';
+import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
+import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
+import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
+
+import { DashboardSection } from 'src/components/dashboard';
+import { Translation } from 'src/components/suite';
import { StakingDashboard } from '../StakingDashboard/StakingDashboard';
+import { ApyCard } from '../StakingDashboard/components/ApyCard';
+import { PayoutCard } from '../StakingDashboard/components/PayoutCard';
+import { ClaimCard } from '../StakingDashboard/components/ClaimCard';
+import { StakingCard } from '../StakingDashboard/components/StakingCard';
interface SolStakingDashboardProps {
- selectedAccount: SelectedAccountStatus;
+ selectedAccount: SelectedAccountLoaded;
}
export const SolStakingDashboard = ({ selectedAccount }: SolStakingDashboardProps) => {
+ const { account } = selectedAccount;
+
+ const isBelowLaptop = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.LG})`);
+
+ const { canClaim = false } = getStakingDataForNetwork(account) ?? {};
+
+ const apy = useSelector((state: StakeRootState) =>
+ selectPoolStatsApyData(state, account?.symbol),
+ );
+
return (
>}
+ dashboard={
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {/* TODO: implement Transactions component */}
+ {/* */}
+
+ }
/>
);
};
diff --git a/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx
index 85871f348a4..6d71c3634d1 100644
--- a/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/StakingDashboard.tsx
@@ -1,4 +1,4 @@
-import { selectAccountHasStaked, selectSolStakingAccounts } from '@suite-common/wallet-core';
+import { selectEthAccountHasStaked, selectSolStakingAccounts } from '@suite-common/wallet-core';
import { SelectedAccountStatus } from '@suite-common/wallet-types';
import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
@@ -14,16 +14,17 @@ interface StakingDashboardProps {
}
export const StakingDashboard = ({ selectedAccount, dashboard }: StakingDashboardProps) => {
- const hasStaked = useSelector(state =>
- selectAccountHasStaked(state, selectedAccount?.account?.key ?? ''),
+ const hasEthStaked = useSelector(state =>
+ selectEthAccountHasStaked(state, selectedAccount.account?.key ?? ''),
+ );
+ const solStakingAccounts = useSelector(state =>
+ selectSolStakingAccounts(state, selectedAccount.account?.key),
);
- const { account } = selectedAccount;
-
- const solStakingAccounts = useSelector(state => selectSolStakingAccounts(state, account?.key));
+ if (selectedAccount.status !== 'loaded') return null;
const hasSolStakingAccount = !!solStakingAccounts?.length;
- const shouldShowDashboard = hasStaked || hasSolStakingAccount;
+ const shouldShowDashboard = hasEthStaked || hasSolStakingAccount;
return (
{
const isClaimPending = useMemo(() => claimTxs.some(tx => isPending(tx)), [claimTxs]);
const { canClaim = false, claimableAmount = '0' } =
- getAccountEverstakeStakingPool(selectedAccount) ?? {};
+ getStakingDataForNetwork(selectedAccount) ?? {};
// Show success message when claim tx confirmation is complete.
const prevIsClaimPending = useRef(false);
diff --git a/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx
index a616668c185..534ed99825b 100644
--- a/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/EmptyStakingCard.tsx
@@ -30,8 +30,7 @@ export const EmptyStakingCard = () => {
const { isStakingDisabled, stakingMessageContent } = useMessageSystemStaking();
- const ethApy = useSelector(state => selectPoolStatsApyData(state, account?.symbol));
- // TODO: calc solApy
+ const apy = useSelector(state => selectPoolStatsApyData(state, account?.symbol));
const dispatch = useDispatch();
const openStakingEthInANutshellModal = () => {
@@ -52,8 +51,8 @@ export const EmptyStakingCard = () => {
(
{
description: ,
},
],
- [ethApy, displaySymbol],
+ [apy, displaySymbol],
);
return (
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/PayoutCard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/PayoutCard.tsx
similarity index 93%
rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/PayoutCard.tsx
rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/PayoutCard.tsx
index 35e5a935727..a41a203ea87 100644
--- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/PayoutCard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/PayoutCard.tsx
@@ -4,7 +4,7 @@ import { BigNumber } from '@trezor/utils/src/bigNumber';
import { Card, Column, Icon, Paragraph } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { BACKUP_REWARD_PAYOUT_DAYS } from '@suite-common/wallet-constants';
-import { getAccountAutocompoundBalance } from '@suite-common/wallet-utils';
+import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
import { Translation } from 'src/components/suite';
import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer';
@@ -23,7 +23,8 @@ export const PayoutCard = ({
}: PayoutCardProps) => {
const selectedAccount = useSelector(selectSelectedAccount);
- const autocompoundBalance = getAccountAutocompoundBalance(selectedAccount);
+ const { autocompoundBalance = '0' } = getStakingDataForNetwork(selectedAccount) ?? {};
+
const payout = useMemo(() => {
if (!nextRewardPayout || !daysToAddToPool) return undefined;
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/ProgressLabel.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/ProgressLabel.tsx
similarity index 100%
rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/ProgressLabel.tsx
rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/ProgressLabel.tsx
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/ProgressLabels.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/ProgressLabels.tsx
similarity index 100%
rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/ProgressLabels.tsx
rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/ProgressLabels.tsx
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/types.ts b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/types.ts
similarity index 100%
rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/ProgressLabels/types.ts
rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/ProgressLabels/types.ts
diff --git a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingCard.tsx b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/StakingCard.tsx
similarity index 86%
rename from packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingCard.tsx
rename to packages/suite/src/views/wallet/staking/components/StakingDashboard/components/StakingCard.tsx
index 80ff767f307..a5ad8af633f 100644
--- a/packages/suite/src/views/wallet/staking/components/EthStakingDashboard/components/StakingCard.tsx
+++ b/packages/suite/src/views/wallet/staking/components/StakingDashboard/components/StakingCard.tsx
@@ -16,7 +16,13 @@ import {
} from '@trezor/components';
import { spacings } from '@trezor/theme';
import { selectAccountStakeTransactions } from '@suite-common/wallet-core';
-import { getAccountEverstakeStakingPool, isPending } from '@suite-common/wallet-utils';
+import {
+ calculateSolanaStakingReward,
+ getStakingAccountCurrentStatus,
+ getStakingDataForNetwork,
+ isPending,
+} from '@suite-common/wallet-utils';
+import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
import { getNetworkDisplaySymbol, type NetworkSymbol } from '@suite-common/wallet-config';
import { FiatValue, Translation, FormattedCryptoAmount } from 'src/components/suite';
@@ -65,12 +71,14 @@ type StakingCardProps = {
isValidatorsQueueLoading?: boolean;
daysToAddToPool?: number;
daysToUnstake?: number;
+ apy?: number;
};
export const StakingCard = ({
isValidatorsQueueLoading,
daysToAddToPool,
daysToUnstake,
+ apy,
}: StakingCardProps) => {
const selectedAccount = useSelector(selectSelectedAccount);
const isBelowLaptop = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.LG})`);
@@ -89,7 +97,7 @@ export const StakingCard = ({
totalPendingStakeBalance = '0',
withdrawTotalAmount = '0',
claimableAmount = '0',
- } = getAccountEverstakeStakingPool(selectedAccount) ?? {};
+ } = getStakingDataForNetwork(selectedAccount) ?? {};
const canUnstake = new BigNumber(autocompoundBalance).gt(0);
const isStakePending = new BigNumber(totalPendingStakeBalance).gt(0);
@@ -110,11 +118,15 @@ export const StakingCard = ({
);
const isStakeConfirming = stakeTxs.some(tx => isPending(tx));
- const { progressLabelsData } = useProgressLabelsData({
+ const solStakingAccountStatus = getStakingAccountCurrentStatus(selectedAccount);
+
+ const progressLabelsData = useProgressLabelsData({
daysToAddToPool,
isDaysToAddToPoolShown,
isStakeConfirming,
isStakePending,
+ selectedAccount,
+ solStakingAccountStatus,
});
const dispatch = useDispatch();
@@ -133,6 +145,10 @@ export const StakingCard = ({
return null;
}
+ const solReward = calculateSolanaStakingReward(depositedBalance, apy?.toString());
+
+ const stakingReward = selectedAccount.networkType === 'solana' ? solReward : restakedReward;
+
return (
@@ -151,11 +167,10 @@ export const StakingCard = ({
data-testid="@account/staking/pending"
/>
)}
-
}
iconName="lock"
- symbol={selectedAccount.symbol}
+ symbol={selectedAccount?.symbol}
cryptoAmount={depositedBalance}
fiatAmount={depositedBalance}
data-testid="@account/staking/staked"
@@ -164,7 +179,16 @@ export const StakingCard = ({
-
-
+
-
{isPendingUnstakeShown && (
-
{
+ const ethereumProgressLabelsData: ProgressLabelData[] = useMemo(
+ () => [
+ {
+ id: 0,
+ progressState: (() => {
+ if (isStakeConfirming) return 'active';
+
+ return 'done';
+ })(),
+ children: isStakeConfirming ? (
+
+ ) : (
+
+ ),
+ },
+ {
+ id: 1,
+ progressState: (() => {
+ if (!isStakeConfirming && isStakePending) return 'active';
+ if (!isStakeConfirming && !isStakePending) return 'done';
+
+ return 'stale';
+ })(),
+ children: (
+
+
+ {isDaysToAddToPoolShown && (
+
+ ~
+
+
+ )}
+
+ ),
+ },
+ {
+ id: 2,
+ progressState: (() => {
+ if (!isStakeConfirming && !isStakePending) {
+ return 'active';
+ }
+
+ return 'stale';
+ })(),
+ children: ,
+ },
+ ],
+ [daysToAddToPool, isDaysToAddToPoolShown, isStakeConfirming, isStakePending],
+ );
+
+ const solanaProgressLabelsData: ProgressLabelData[] = useMemo(
+ () => [
+ {
+ id: 0,
+ progressState: (() => {
+ if (solStakingAccountStatus === 'inactive') return 'active';
+
+ return 'done';
+ })(),
+ children: isStakeConfirming ? (
+
+ ) : (
+
+ ),
+ },
+ {
+ id: 1,
+ progressState: (() => {
+ if (solStakingAccountStatus === 'activating') return 'active';
+ if (solStakingAccountStatus !== 'activating') return 'done';
+
+ return 'stale';
+ })(),
+ children: (
+
+
+
+
+ ~
+
+
+
+ ),
+ },
+ {
+ id: 2,
+ progressState: (() => {
+ if (solStakingAccountStatus === 'active') {
+ return 'active';
+ }
+
+ return 'stale';
+ })(),
+ children: (
+
+
+
+
+ ~
+
+
+
+ ),
+ },
+ ],
+ [solStakingAccountStatus, isStakeConfirming],
+ );
+
+ switch (selectedAccount?.networkType) {
+ case 'ethereum':
+ return ethereumProgressLabelsData;
+ case 'solana':
+ return solanaProgressLabelsData;
+ default:
+ return [];
+ }
+};
diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts
index 308cbddf7d6..2bf689cb709 100644
--- a/packages/urls/src/urls.ts
+++ b/packages/urls/src/urls.ts
@@ -96,6 +96,8 @@ export const HELP_CENTER_DEVICE_AUTHENTICATION: Url =
'https://trezor.io/learn/a/trezor-safe-device-authentication-check';
export const HELP_CENTER_ETH_STAKING: Url =
'https://trezor.io/learn/a/stake-ethereum-eth-in-trezor-suite';
+export const HELP_CENTER_SOL_STAKING: Url =
+ 'https://trezor.io/learn/a/stake-solana-sol-in-trezor-suite';
export const HELP_CENTER_SEED_CARD_URL: Url = 'https://trezor.io/learn/a/recovery-seed-card';
export const HELP_CENTER_MULTI_SHARE_BACKUP_URL: Url =
'https://trezor.io/learn/a/multi-share-backup-on-trezor';
diff --git a/suite-common/wallet-constants/src/solanaStakingConstants.ts b/suite-common/wallet-constants/src/solanaStakingConstants.ts
index 02b4d4f12ac..a5dd6362051 100644
--- a/suite-common/wallet-constants/src/solanaStakingConstants.ts
+++ b/suite-common/wallet-constants/src/solanaStakingConstants.ts
@@ -1,9 +1,9 @@
import { BigNumber } from '@trezor/utils';
-// TODO: change to solana constants
-export const MIN_SOL_AMOUNT_FOR_STAKING = new BigNumber(0.1);
-export const MAX_SOL_AMOUNT_FOR_STAKING = new BigNumber(1_000_000);
-export const MIN_SOL_FOR_WITHDRAWALS = new BigNumber(0.03);
+export const BACKUP_SOL_APY = 6.87;
+export const MIN_SOL_AMOUNT_FOR_STAKING = new BigNumber(0.01);
+export const MAX_SOL_AMOUNT_FOR_STAKING = new BigNumber(10_000_000);
+export const MIN_SOL_FOR_WITHDRAWALS = new BigNumber(0.000005);
export const MIN_SOL_BALANCE_FOR_STAKING = MIN_SOL_AMOUNT_FOR_STAKING.plus(MIN_SOL_FOR_WITHDRAWALS);
-export const LAMPORTS_PER_SOL = 1_000_000_000;
+export const SOLANA_EPOCH_DAYS = 3;
diff --git a/suite-common/wallet-constants/src/stakingConstants.ts b/suite-common/wallet-constants/src/stakingConstants.ts
index f9edbf858a2..98cfac37cdc 100644
--- a/suite-common/wallet-constants/src/stakingConstants.ts
+++ b/suite-common/wallet-constants/src/stakingConstants.ts
@@ -3,3 +3,5 @@
// It is a constant which allows the SDK to define which app calls its functions.
// Each app which integrates the SDK has its own source, e.g. source for Trezor Suite is '1'.
export const WALLET_SDK_SOURCE = '1';
+
+export const BACKUP_APY = 4.13;
diff --git a/suite-common/wallet-core/src/transactions/transactionsReducer.ts b/suite-common/wallet-core/src/transactions/transactionsReducer.ts
index 83a4399aed9..e706ba0c737 100644
--- a/suite-common/wallet-core/src/transactions/transactionsReducer.ts
+++ b/suite-common/wallet-core/src/transactions/transactionsReducer.ts
@@ -374,7 +374,7 @@ export const selectAccountClaimTransactions = createMemoizedSelector(
),
);
-export const selectAccountHasStaked = createMemoizedSelector(
+export const selectEthAccountHasStaked = createMemoizedSelector(
[selectAccountStakeTransactions, selectAccountByKey],
(stakeTxs, account) => {
if (!account) return false;
@@ -386,7 +386,7 @@ export const selectAccountHasStaked = createMemoizedSelector(
export const selectAssetAccountsThatStaked = (
state: TransactionsRootState & AccountsRootState,
accounts: Account[],
-) => accounts.filter(account => selectAccountHasStaked(state, account.key));
+) => accounts.filter(account => selectEthAccountHasStaked(state, account.key));
export const selectAccountTransactionsFetchStatus = (
state: TransactionsRootState,
diff --git a/suite-common/wallet-utils/src/solanaStakingUtils.ts b/suite-common/wallet-utils/src/solanaStakingUtils.ts
index 2eeb5c12c8c..8a144beed25 100644
--- a/suite-common/wallet-utils/src/solanaStakingUtils.ts
+++ b/suite-common/wallet-utils/src/solanaStakingUtils.ts
@@ -1,6 +1,6 @@
-import { Solana, SolNetwork } from '@everstake/wallet-sdk';
+import { Solana, SolDelegation, SolNetwork, StakeAccount, StakeState } from '@everstake/wallet-sdk';
-import { NetworkSymbol } from '@suite-common/wallet-config';
+import { getNetworkFeatures, NetworkSymbol } from '@suite-common/wallet-config';
import { BigNumber, isArrayMember } from '@trezor/utils';
import { SolanaStakingAccount } from '@trezor/blockchain-link-types/src/solana';
import {
@@ -8,8 +8,10 @@ import {
supportedSolanaNetworkSymbols,
SupportedSolanaNetworkSymbols,
} from '@suite-common/wallet-types';
-import { LAMPORTS_PER_SOL } from '@suite-common/wallet-constants';
import { PartialRecord } from '@trezor/type-utils';
+import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
+
+import { formatNetworkAmount } from './accountUtils';
export function isSupportedSolStakingNetworkSymbol(
symbol: NetworkSymbol,
@@ -17,11 +19,17 @@ export function isSupportedSolStakingNetworkSymbol(
return isArrayMember(symbol, supportedSolanaNetworkSymbols);
}
-export const formatSolanaStakingAmount = (amount: string | null) => {
- if (!amount) return null;
+export const getSolanaStakingSymbols = (networkSymbols: NetworkSymbol[]) =>
+ networkSymbols.reduce((acc, networkSymbol) => {
+ if (
+ isSupportedSolStakingNetworkSymbol(networkSymbol) &&
+ getNetworkFeatures(networkSymbol).includes('staking')
+ ) {
+ acc.push(networkSymbol);
+ }
- return new BigNumber(amount).div(LAMPORTS_PER_SOL).toFixed(9);
-};
+ return acc;
+ }, [] as SupportedSolanaNetworkSymbols[]);
interface SolNetworkConfig {
network: SolNetwork;
@@ -67,6 +75,108 @@ export const getSolAccountTotalStakingBalance = (account: Account) => {
if (!solStakingAccounts) return null;
const totalStakingBalance = calculateTotalSolStakingBalance(solStakingAccounts);
+ if (!totalStakingBalance) return null;
+
+ return formatNetworkAmount(totalStakingBalance, account.symbol);
+};
+
+export const calculateSolanaStakingReward = (accountBalance?: string, apy?: string) => {
+ if (!accountBalance || !apy) return '0';
+
+ return new BigNumber(accountBalance ?? '')
+ .multipliedBy(apy ?? '0')
+ .dividedBy(100)
+ .dividedBy(365)
+ .multipliedBy(SOLANA_EPOCH_DAYS)
+ .toFixed(9)
+ .toString();
+};
+
+export const getStakingAccountStatus = (
+ solStakingAccount: SolDelegation['account'],
+ epoch: number,
+) => {
+ const stakeAccountClient = new StakeAccount(solStakingAccount);
+
+ return stakeAccountClient.stakeAccountState(epoch);
+};
+
+interface StakingAccountWithStatus extends SolDelegation {
+ status: string;
+}
+
+export const getSolanaStakingAccountsWithStatus = (
+ account: Account,
+): StakingAccountWithStatus[] | null => {
+ if (account.networkType !== 'solana') return null;
+
+ const { solStakingAccounts, solEpoch } = account.misc;
+ if (!solStakingAccounts?.length || !solEpoch) return null;
+
+ const stakingAccountsWithStatus = solStakingAccounts.map(solStakingAccount => {
+ const status = getStakingAccountStatus(solStakingAccount.account, solEpoch);
+
+ return {
+ ...solStakingAccount,
+ status,
+ };
+ });
+
+ return stakingAccountsWithStatus;
+};
- return formatSolanaStakingAmount(totalStakingBalance);
+export const getSolanaStakingAccountsByStatus = (account: Account, status: string) => {
+ const stakingAccountsWithStatus = getSolanaStakingAccountsWithStatus(account);
+
+ if (!stakingAccountsWithStatus) return [];
+
+ return stakingAccountsWithStatus.filter(
+ solStakingAccount => solStakingAccount.status === status,
+ );
+};
+
+export const getStakingAccountCurrentStatus = (account?: Account) => {
+ if (account?.networkType !== 'solana') return null;
+
+ const statusesToCheck = [StakeState.inactive, StakeState.activating];
+
+ for (const status of statusesToCheck) {
+ const stakingAccounts = getSolanaStakingAccountsByStatus(account, status);
+ if (stakingAccounts.length) return status;
+ }
+
+ return null;
+};
+
+export const getSolStakingAccountTotalBalanceByStatus = (account: Account, status: string) => {
+ if (account.networkType !== 'solana') return '0';
+
+ const selectedStakingAccounts = getSolanaStakingAccountsByStatus(account, status);
+ const stakingBalance = calculateTotalSolStakingBalance(selectedStakingAccounts) ?? '0';
+
+ return formatNetworkAmount(stakingBalance, account.symbol);
+};
+
+type StakeStateType = (typeof StakeState)[keyof typeof StakeState];
+
+export const getSolStakingAccountsInfo = (account: Account) => {
+ const balanceResults = Object.entries(StakeState).map(([key, status]) => {
+ const balance = getSolStakingAccountTotalBalanceByStatus(account, status);
+
+ return [key, balance];
+ });
+
+ const balances: Record = balanceResults.reduce(
+ (acc, [key, balance]) => ({ ...acc, [key]: balance }),
+ {},
+ );
+
+ return {
+ solStakedBalance: balances[StakeState.active],
+ solClaimableBalance: balances[StakeState.deactivated],
+ solPendingStakeBalance: balances[StakeState.activating],
+ solPendingUnstakeBalance: balances[StakeState.deactivating],
+ canClaimSol: new BigNumber(balances[StakeState.deactivated]).gt(0),
+ canUnstakeSol: new BigNumber(balances[StakeState.active]).gt(0),
+ };
};