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 committed Dec 23, 2024
1 parent 6a42466 commit 6249141
Show file tree
Hide file tree
Showing 33 changed files with 576 additions and 163 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
62 changes: 53 additions & 9 deletions packages/suite/src/components/suite/StakingProcess/StakingInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,57 @@ import {
selectPoolStatsApyData,
AccountsRootState,
} from '@suite-common/wallet-core';
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 @@ -39,30 +83,30 @@ export const StakingInfo = ({ isExpanded }: StakingInfoProps) => {
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={{ symbol: account.symbol.toUpperCase() }}
/>
),
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.` },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,49 @@ import {
StakeRootState,
AccountsRootState,
} from '@suite-common/wallet-core';
import { 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 @@ -35,6 +71,7 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {

const daysToUnstake = getDaysToUnstake(unstakeTxs, data);
const accountSymbol = account.symbol.toUpperCase();
const infoRowsData = getInfoRowsData(account.networkType, account.symbol, daysToUnstake);

const infoRows = [
{
Expand All @@ -45,15 +82,15 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {
},
},
{
heading: <Translation id="TR_STAKE_LEAVE_STAKING_POOL" />,
subheading: (
<Translation
id="TR_STAKING_CONSOLIDATING_FUNDS"
values={{ symbol: accountSymbol }}
/>
),
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 @@ -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 { Fees } from 'src/components/wallet/Fees/Fees';
Expand Down Expand Up @@ -44,7 +44,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 @@ -4,7 +4,7 @@ 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';
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
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -70,7 +70,7 @@ export const Options = ({ symbol }: OptionsProps) => {
autocompoundBalance = '0',
depositedBalance = '0',
restakedReward = '0',
} = getAccountEverstakeStakingPool(selectedAccount) ?? {};
} = getStakingDataForNetwork(selectedAccount) ?? {};

return (
<Column gap={spacings.sm}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Account } from '@suite-common/wallet-types';
import { selectCoinDefinitions } from '@suite-common/token-definitions';
import { selectAccountHasStaked, selectSolStakingAccounts } from '@suite-common/wallet-core';
import { selectEthAccountHasStaked, selectSolStakingAccounts } from '@suite-common/wallet-core';
import { isSupportedStakingNetworkSymbol } from '@suite-common/wallet-utils';

import { useSelector } from 'src/hooks/suite';
Expand Down Expand Up @@ -36,13 +36,13 @@ export const AccountSection = ({
const isDebugModeActive = useSelector(selectIsDebugModeActive);

const coinDefinitions = useSelector(state => 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);

Expand Down
4 changes: 2 additions & 2 deletions packages/suite/src/hooks/wallet/useUnstakeEthForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 6249141

Please sign in to comment.