Skip to content

Commit

Permalink
feat(suite): implement solana staking dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-pvl authored and tomasklim committed Dec 28, 2024
1 parent 1cb198e commit f059e14
Show file tree
Hide file tree
Showing 39 changed files with 627 additions and 212 deletions.
1 change: 1 addition & 0 deletions packages/suite-desktop-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
20 changes: 18 additions & 2 deletions packages/suite/src/components/suite/CoinList/CoinList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand All @@ -50,7 +53,14 @@ export const CoinList = ({
return (
<Wrapper>
{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 =
Expand All @@ -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 (
<Tooltip
Expand Down
67 changes: 55 additions & 12 deletions packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,57 @@ import {
selectPoolStatsApyData,
AccountsRootState,
} from '@suite-common/wallet-core';
import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';
import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';

import { Translation } from 'src/components/suite';
import { getDaysToAddToPool } from 'src/utils/suite/ethereumStaking';
import { CoinjoinRootState } from 'src/reducers/wallet/coinjoinReducer';

import { InfoRow } from './InfoRow';

type InfoRowsData = {
payoutDays: number | undefined;
rewardsPeriodHeading: JSX.Element;
rewardsPeriodSubheading: JSX.Element;
rewardsEarningHeading: JSX.Element;
};

const getInfoRowsData = (
networkType: NetworkType,
accountSymbol: NetworkSymbol,
daysToAddToPool?: number,
): InfoRowsData | null => {
switch (networkType) {
case 'ethereum':
return {
payoutDays: daysToAddToPool,
rewardsPeriodHeading: <Translation id="TR_STAKE_ENTER_THE_STAKING_POOL" />,
rewardsPeriodSubheading: (
<Translation
id="TR_STAKING_GETTING_READY"
values={{ symbol: accountSymbol.toUpperCase() }}
/>
),
rewardsEarningHeading: <Translation id="TR_STAKE_EARN_REWARDS_WEEKLY" />,
};
case 'solana':
return {
payoutDays: SOLANA_EPOCH_DAYS,
rewardsPeriodHeading: <Translation id="TR_STAKE_WARM_UP_PERIOD" />,
rewardsPeriodSubheading: <Translation id="TR_STAKE_WAIT_FOR_ACTIVATION" />,
rewardsEarningHeading: (
<Translation
id="TR_STAKE_EARN_REWARDS_EVERY"
values={{ days: SOLANA_EPOCH_DAYS }}
/>
),
};
default:
return null;
}
};

interface StakingInfoProps {
isExpanded?: boolean;
}
Expand All @@ -33,39 +76,39 @@ 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 = [
{
heading: <Translation id="TR_STAKE_SIGN_TRANSACTION" />,
content: { text: <Translation id="TR_COINMARKET_NETWORK_FEE" />, isBadge: true },
},
{
heading: <Translation id="TR_STAKE_ENTER_THE_STAKING_POOL" />,
subheading: (
<Translation
id="TR_STAKING_GETTING_READY"
values={{ networkSymbol: getNetworkDisplaySymbol(account.symbol) }}
/>
),
heading: infoRowsData?.rewardsPeriodHeading,
subheading: infoRowsData?.rewardsPeriodSubheading,
content: {
text: (
<>
~<Translation id="TR_STAKE_DAYS" values={{ count: daysToAddToPool }} />
~
<Translation
id="TR_STAKE_DAYS"
values={{ count: infoRowsData?.payoutDays }}
/>
</>
),
},
},
{
heading: <Translation id="TR_STAKE_EARN_REWARDS_WEEKLY" />,
heading: infoRowsData?.rewardsEarningHeading,
subheading: <Translation id="TR_STAKING_REWARDS_ARE_RESTAKED" />,
content: { text: `~${ethApy}% p.a.` },
content: { text: `~${apy}% p.a.` },
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,49 @@ 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';
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: <Translation id="TR_STAKE_LEAVE_STAKING_POOL" />,
deactivatePeriodSubheading: (
<Translation
id="TR_STAKING_CONSOLIDATING_FUNDS"
values={{ symbol: accountSymbol.toUpperCase() }}
/>
),
};
case 'solana':
return {
readyForClaimDays: SOLANA_EPOCH_DAYS,
deactivatePeriodHeading: <Translation id="TR_STAKE_COOL_DOWN_PERIOD" />,
deactivatePeriodSubheading: <Translation id="TR_STAKE_WAIT_FOR_DEACTIVATION" />,
};
default:
return null;
}
};

interface UnstakingInfoProps {
isExpanded?: boolean;
}
Expand All @@ -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 = [
{
Expand All @@ -46,15 +82,15 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {
},
},
{
heading: <Translation id="TR_STAKE_LEAVE_STAKING_POOL" />,
subheading: (
<Translation
id="TR_STAKING_CONSOLIDATING_FUNDS"
values={{ networkSymbol: displaySymbol }}
/>
),
heading: infoRowsData?.deactivatePeriodHeading,
subheading: infoRowsData?.deactivatePeriodSubheading,
content: {
text: <Translation id="TR_STAKE_DAYS" values={{ count: daysToUnstake }} />,
text: (
<Translation
id="TR_STAKE_DAYS"
values={{ count: infoRowsData?.readyForClaimDays }}
/>
),
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
<Modal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect } from 'react';

import { Paragraph, Tooltip, Banner, Card, Column, InfoItem, NewModal } from '@trezor/components';
import { spacings } from '@trezor/theme';
import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils';
import { getStakingDataForNetwork } from '@suite-common/wallet-utils';
import type { SelectedAccountLoaded } from '@suite-common/wallet-types';
import { getNetworkDisplaySymbol } from '@suite-common/wallet-config';

Expand Down Expand Up @@ -45,7 +45,7 @@ export const ClaimModal = ({ onCancel }: ClaimModalModalProps) => {
// 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -22,30 +22,30 @@ 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: <Translation id="TR_STAKE_WEEKLY" />,
value: calculateGains(cryptoInput, ethApy, 52),
value: calculateGains(cryptoInput, apy, 52),
},
{
label: <Translation id="TR_STAKE_MONTHLY" />,
value: calculateGains(cryptoInput, ethApy, 12),
value: calculateGains(cryptoInput, apy, 12),
},
{
label: <Translation id="TR_STAKE_YEARLY" />,
value: calculateGains(cryptoInput, ethApy, 1),
value: calculateGains(cryptoInput, apy, 1),
},
];

return (
<Column gap={spacings.lg}>
<Column>
<Paragraph variant="primary" typographyStyle="titleMedium">
{ethApy}%
{apy}%
</Paragraph>
<Paragraph
typographyStyle="hint"
Expand Down Expand Up @@ -74,7 +74,15 @@ export const EstimatedGains = () => {
id="TR_STAKING_YOUR_EARNINGS"
values={{
a: chunks => (
<TrezorLink href={HELP_CENTER_ETH_STAKING}>{chunks}</TrezorLink>
<TrezorLink
href={
account.networkType === 'solana'
? HELP_CENTER_SOL_STAKING
: HELP_CENTER_ETH_STAKING
}
>
{chunks}
</TrezorLink>
),
}}
/>
Expand Down
Loading

0 comments on commit f059e14

Please sign in to comment.