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 #16027

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion packages/blockchain-link-types/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ export interface AccountInfo {
nonce?: string;
contractInfo?: ContractInfo;
stakingPools?: StakingPool[];
solStakingAccounts?: SolanaStakingAccount[]; // solana staking accounts
addressAliases?: { [key: string]: AddressAlias };
// XRP
sequence?: number;
Expand All @@ -243,6 +242,8 @@ export interface AccountInfo {
// SOL
owner?: string; // The Solana program owning the account
rent?: number; // The rent required for the account to opened
solStakingAccounts?: SolanaStakingAccount[]; // Solana staking accounts
solEpoch?: number; // Solana current epoch
};
page?: {
// blockbook and blockfrost
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels odd to have blockchain related info (network epoch) with account related data 🤔

};
}
}
Expand Down Expand Up @@ -708,7 +707,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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be called for each account instead of fetching that info only once for the whole network - imo this should not be fetched nor stored with account data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Where do you think it should be fetched/stored instead?

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

return { stakingAccounts, epoch };
};
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
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
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
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
Loading
Loading