Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(suite): solana staking dashboard implementation #16044

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/blockchain-link-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"prepublish": "yarn tsx ../../scripts/prepublish.js"
},
"dependencies": {
"@everstake/wallet-sdk": "^1.0.5",
"@everstake/wallet-sdk": "^1.0.7",
"@solana/web3.js": "^2.0.0",
"@trezor/type-utils": "workspace:*",
"@trezor/utxo-lib": "workspace:*"
Expand Down
1 change: 1 addition & 0 deletions packages/blockchain-link-types/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export interface AccountInfo {
contractInfo?: ContractInfo;
stakingPools?: StakingPool[];
solStakingAccounts?: SolanaStakingAccount[]; // solana staking accounts
solEpoch?: number;
addressAliases?: { [key: string]: AddressAlias };
// XRP
sequence?: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/blockchain-link-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"prepublish": "yarn tsx ../../scripts/prepublish.js"
},
"dependencies": {
"@everstake/wallet-sdk": "^1.0.5",
"@everstake/wallet-sdk": "^1.0.7",
"@mobily/ts-belt": "^3.13.1",
"@trezor/env-utils": "workspace:*",
"@trezor/utils": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/blockchain-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"worker-loader": "^3.0.8"
},
"dependencies": {
"@everstake/wallet-sdk": "^1.0.5",
"@everstake/wallet-sdk": "^1.0.7",
"@solana-program/token": "^0.4.1",
"@solana-program/token-2022": "^0.3.1",
"@solana/web3.js": "^2.0.0",
Expand Down
13 changes: 6 additions & 7 deletions packages/blockchain-link/src/workers/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import { IntervalId } from '@trezor/type-utils';

import { getBaseFee, getPriorityFee } from './fee';
import { BaseWorker, ContextType, CONTEXT } from '../baseWorker';
// import { getSolanaStakingAccounts } from '../utils';
import { getSolanaStakingData } from '../utils';

export type SolanaAPI = Readonly<{
clusterUrl: ClusterUrl;
Expand Down Expand Up @@ -208,8 +208,7 @@ const pushTransaction = async (request: Request<MessageTypes.PushTransaction>) =

const getAccountInfo = async (
request: Request<MessageTypes.GetAccountInfo>,
// TODO: uncomment when solana staking accounts are supported
// isTestnet: boolean,
isTestnet: boolean,
) => {
const { payload } = request;
const { details = 'basic' } = payload;
Expand Down Expand Up @@ -351,12 +350,12 @@ const getAccountInfo = async (
const accountDataBytes = getBase64Encoder().encode(accountDataEncoded);
const accountDataLength = BigInt(accountDataBytes.byteLength);
const rent = await api.rpc.getMinimumBalanceForRentExemption(accountDataLength).send();
// TODO: uncomment when solana staking accounts are supported
// const stakingAccounts = await getSolanaStakingAccounts(payload.descriptor, isTestnet);
const stakingData = await getSolanaStakingData(payload.descriptor, isTestnet);
misc = {
owner: accountInfo?.owner,
rent: Number(rent),
solStakingAccounts: [],
solStakingAccounts: stakingData?.stakingAccounts,
solEpoch: stakingData?.epoch,
};
}
}
Expand Down Expand Up @@ -707,7 +706,7 @@ const unsubscribe = (request: Request<MessageTypes.Unsubscribe>) => {
const onRequest = (request: Request<MessageTypes.Message>, isTestnet: boolean) => {
switch (request.type) {
case MESSAGES.GET_ACCOUNT_INFO:
return getAccountInfo(request);
return getAccountInfo(request, isTestnet);
case MESSAGES.GET_INFO:
return getInfo(request, isTestnet);
case MESSAGES.PUSH_TRANSACTION:
Expand Down
13 changes: 11 additions & 2 deletions packages/blockchain-link/src/workers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const prioritizeEndpoints = (urls: string[]) =>
.sort(([, a], [, b]) => b - a)
.map(([url]) => url);

export const getSolanaStakingAccounts = async (descriptor: string, isTestnet: boolean) => {
export const getSolanaStakingData = async (descriptor: string, isTestnet: boolean) => {
const blockchainEnvironment = isTestnet ? 'devnet' : 'mainnet';

// Find the blockchain configuration for the specified chain and environment
Expand All @@ -38,7 +38,16 @@ export const getSolanaStakingAccounts = async (descriptor: string, isTestnet: bo
const solanaClient = new Solana(network, serverUrl);

const delegations = await solanaClient.getDelegations(descriptor);
if (!delegations || !delegations.result) {
throw new Error('Failed to fetch delegations');
}
const { result: stakingAccounts } = delegations;

return stakingAccounts;
const epochInfo = await solanaClient.getEpochInfo();
if (!epochInfo || !epochInfo.result) {
throw new Error('Failed to fetch epoch info');
}
const { epoch } = epochInfo.result;

return { stakingAccounts, epoch };
};
2 changes: 1 addition & 1 deletion packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"test-unit:watch": "yarn g:jest -o --watch"
},
"dependencies": {
"@everstake/wallet-sdk": "^1.0.5",
"@everstake/wallet-sdk": "^1.0.7",
"@floating-ui/react": "^0.26.9",
"@formatjs/intl": "2.10.0",
"@hookform/resolvers": "3.9.1",
Expand Down
30 changes: 28 additions & 2 deletions packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ import {
MIN_SOL_AMOUNT_FOR_STAKING,
MIN_SOL_BALANCE_FOR_STAKING,
MIN_SOL_FOR_WITHDRAWALS,
SOL_STAKING_OPERATION_FEE,
} from '@suite-common/wallet-constants';

import { Dispatch, GetState } from 'src/types/suite';
import { selectAddressDisplayType } from 'src/reducers/suite/suiteReducer';
import { getPubKeyFromAddress, prepareStakeSolTx } from 'src/utils/suite/solanaStaking';
import {
getPubKeyFromAddress,
prepareClaimSolTx,
prepareStakeSolTx,
prepareUnstakeSolTx,
} from 'src/utils/suite/solanaStaking';

import { calculate, composeStakingTransaction } from './stakeFormActions';

Expand All @@ -30,7 +36,8 @@ const calculateTransaction = (
compareWithAmount = true,
symbol: NetworkSymbol,
): PrecomposedTransaction => {
const feeInLamports = new BigNumber(feeLevel.feePerTx ?? '0').toString();
// TODO: change to the dynamic fee
const feeInLamports = new BigNumber(SOL_STAKING_OPERATION_FEE).toString();

const stakingParams = {
feeInBaseUnits: feeInLamports,
Expand Down Expand Up @@ -100,6 +107,25 @@ export const signTransaction =
});
}

if (stakeType === 'unstake') {
txData = await prepareUnstakeSolTx({
from: account.descriptor,
path: account.path,
amount: formValues.outputs[0].amount,
symbol: account.symbol,
selectedBlockchain,
});
}

if (stakeType === 'claim') {
txData = await prepareClaimSolTx({
from: account.descriptor,
path: account.path,
symbol: account.symbol,
selectedBlockchain,
});
}

if (!txData) {
dispatch(
notificationsActions.addToast({
Expand Down
26 changes: 14 additions & 12 deletions packages/suite/src/components/suite/FormFractionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import styled from 'styled-components';

import { Button, Tooltip } from '@trezor/components';
import { BigNumber } from '@trezor/utils/src/bigNumber';
import { MIN_ETH_AMOUNT_FOR_STAKING } from '@suite-common/wallet-constants';
import { NetworkSymbol } from '@suite-common/wallet-config';
import { Account } from '@suite-common/wallet-types';
import { getStakingLimitsByNetwork } from '@suite-common/wallet-utils';

import { Translation } from 'src/components/suite';

Expand All @@ -24,32 +24,34 @@ interface FormFractionButtonsProps {
setRatioAmount: (divisor: number) => void;
setMax: () => void;
isDisabled?: boolean;
symbol: NetworkSymbol;
totalAmount?: number | string;
account: Account;
decimals?: number;
}

export const FormFractionButtons = ({
setRatioAmount,
setMax,
isDisabled = false,
symbol,
totalAmount,
account,
decimals,
}: FormFractionButtonsProps) => {
const { symbol, formattedBalance: totalAmount } = account;

const { MIN_AMOUNT_FOR_STAKING } = getStakingLimitsByNetwork(account);

const isFractionButtonDisabled = (divisor: number) => {
if (!totalAmount || !decimals) return false;

return new BigNumber(totalAmount)
.dividedBy(divisor)
.decimalPlaces(decimals)
.lte(MIN_ETH_AMOUNT_FOR_STAKING);
.lte(MIN_AMOUNT_FOR_STAKING);
};
const is10PercentDisabled = isDisabled || isFractionButtonDisabled(10);
const is25PercentDisabled = isDisabled || isFractionButtonDisabled(4);
const is50PercentDisabled = isDisabled || isFractionButtonDisabled(2);
const isMaxDisabled =
isDisabled || new BigNumber(totalAmount || '0').lt(MIN_ETH_AMOUNT_FOR_STAKING);
isDisabled || new BigNumber(totalAmount || '0').lt(MIN_AMOUNT_FOR_STAKING);

return (
<Flex>
Expand All @@ -59,7 +61,7 @@ export const FormFractionButtons = ({
<Translation
id="TR_STAKE_MIN_AMOUNT_TOOLTIP"
values={{
amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(),
amount: MIN_AMOUNT_FOR_STAKING.toString(),
symbol: symbol.toUpperCase(),
}}
/>
Expand All @@ -77,7 +79,7 @@ export const FormFractionButtons = ({
<Translation
id="TR_STAKE_MIN_AMOUNT_TOOLTIP"
values={{
amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(),
amount: MIN_AMOUNT_FOR_STAKING.toString(),
symbol: symbol.toUpperCase(),
}}
/>
Expand All @@ -95,7 +97,7 @@ export const FormFractionButtons = ({
<Translation
id="TR_STAKE_MIN_AMOUNT_TOOLTIP"
values={{
amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(),
amount: MIN_AMOUNT_FOR_STAKING.toString(),
symbol: symbol.toUpperCase(),
}}
/>
Expand All @@ -113,7 +115,7 @@ export const FormFractionButtons = ({
<Translation
id="TR_STAKE_MIN_AMOUNT_TOOLTIP"
values={{
amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(),
amount: MIN_AMOUNT_FOR_STAKING.toString(),
symbol: symbol.toUpperCase(),
}}
/>
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
Loading
Loading