Skip to content

Commit

Permalink
feat(suite): solana staking fee estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-pvl authored and tomasklim committed Feb 21, 2025
1 parent 42592f6 commit 7670a6a
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 49 deletions.
12 changes: 11 additions & 1 deletion packages/suite/src/actions/wallet/stake/stakeFormActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
formatAmount,
getExternalComposeOutput,
} from '@suite-common/wallet-utils';
import { Fee } from '@trezor/blockchain-link-types/src/blockbook';
import { FeeLevel } from '@trezor/connect';
import { BigNumber } from '@trezor/utils/src/bigNumber';

Expand Down Expand Up @@ -115,7 +116,9 @@ export const composeStakingTransaction = (
feeLevel: FeeLevel,
compareWithAmount: boolean,
symbol: NetworkSymbol,
estimatedFee?: Fee[number],
) => PrecomposedTransaction,
estimatedFee?: Fee[number],
customFeeLimit?: string,
) => {
const { account, network } = formState;
Expand All @@ -129,7 +132,14 @@ export const composeStakingTransaction = (
const wrappedResponse: PrecomposedLevels = {};
const compareWithAmount = formValues.stakeType === 'stake';
const response = predefinedLevels.map(level =>
calculateTransaction(availableBalance, output, level, compareWithAmount, account.symbol),
calculateTransaction(
availableBalance,
output,
level,
compareWithAmount,
account.symbol,
estimatedFee,
),
);
response.forEach((tx, index) => {
const feeLabel = predefinedLevels[index].label as FeeLevel['label'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const composeTransaction =
formState,
predefinedLevels,
calculateTransaction,
undefined,
customFeeLimit,
);
};
Expand Down
157 changes: 122 additions & 35 deletions packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@ import {
} from '@suite-common/wallet-constants';
import { ComposeActionContext, selectSelectedDevice } from '@suite-common/wallet-core';
import {
Account,
AddressDisplayOptions,
BlockchainNetworks,
ExternalOutput,
PrecomposedTransaction,
PrecomposedTransactionFinal,
SelectedAccountStatus,
StakeFormState,
} from '@suite-common/wallet-types';
import { networkAmountToSmallestUnit } from '@suite-common/wallet-utils';
import { Fee } from '@trezor/blockchain-link-types/src/blockbook';
import TrezorConnect, { FeeLevel } from '@trezor/connect';
import { BigNumber } from '@trezor/utils/src/bigNumber';

import { selectAddressDisplayType } from 'src/reducers/suite/suiteReducer';
import { Dispatch, GetState } from 'src/types/suite';
import {
PrepareStakeSolTxResponse,
prepareClaimSolTx,
prepareStakeSolTx,
prepareUnstakeSolTx,
Expand All @@ -36,9 +41,10 @@ const calculateTransaction = (
feeLevel: FeeLevel,
compareWithAmount = true,
symbol: NetworkSymbol,
estimatedFee?: Fee[number],
): PrecomposedTransaction => {
// TODO: change to the dynamic fee
const feeInLamports = new BigNumber(SOL_STAKING_OPERATION_FEE).toString();
const feeInLamports =
estimatedFee?.feePerTx ?? new BigNumber(SOL_STAKING_OPERATION_FEE).toString();

const stakingParams = {
feeInBaseUnits: feeInLamports,
Expand All @@ -56,11 +62,111 @@ const calculateTransaction = (
),
};

return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams);
const estimatedFeeLevel = { ...feeLevel, ...estimatedFee };

return calculate(
availableBalance,
output,
estimatedFeeLevel,
compareWithAmount,
symbol,
stakingParams,
);
};

const getTransactionData = async (
formValues: StakeFormState,
selectedAccount: SelectedAccountStatus,
blockchain: BlockchainNetworks,
estimatedFee?: Fee[number],
) => {
const { stakeType } = formValues;

if (selectedAccount.status !== 'loaded') return;

const { account } = selectedAccount;
if (account.networkType !== 'solana') return;

const selectedBlockchain = blockchain[account.symbol];

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

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

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

return txData;
};

async function estimateFee(
account: Account,
txData?: PrepareStakeSolTxResponse,
): Promise<Fee[number] | undefined> {
if (!txData?.success) return undefined;

const estimatedFee = await TrezorConnect.blockchainEstimateFee({
coin: account.symbol,
request: {
specific: {
data: txData.tx.txShim.serialize(),
isCreatingAccount: false,
newTokenAccountProgramName: undefined,
},
},
});

if (estimatedFee && estimatedFee.payload && 'levels' in estimatedFee.payload) {
const { levels } = estimatedFee.payload;

return levels[0];
}

return undefined;
}

export const composeTransaction =
(formValues: StakeFormState, formState: ComposeActionContext) => () => {
(formValues: StakeFormState, formState: ComposeActionContext) =>
async (_: Dispatch, getState: GetState) => {
const { selectedAccount, blockchain } = getState().wallet;

if (selectedAccount.status !== 'loaded') return;

const { account } = selectedAccount;
const txData = await getTransactionData(formValues, selectedAccount, blockchain);

let estimatedFee;
// it is not needed to estimate fee for empty input
if (formValues.cryptoInput) {
estimatedFee = await estimateFee(account, txData);
}

const { feeInfo } = formState;
if (!feeInfo) return;

Expand All @@ -72,6 +178,7 @@ export const composeTransaction =
formState,
predefinedLevels,
calculateTransaction,
estimatedFee,
undefined,
);
};
Expand All @@ -93,39 +200,19 @@ export const signTransaction =
const { account } = selectedAccount;
if (account.networkType !== 'solana') return;

const selectedBlockchain = blockchain[account.symbol];
const addressDisplayType = selectAddressDisplayType(getState());
const { stakeType } = formValues;

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

if (stakeType === 'unstake') {
txData = await prepareUnstakeSolTx({
from: account.descriptor,
path: account.path,
amount: formValues.outputs[0].amount,
symbol: account.symbol,
selectedBlockchain,
});
}
const estimatedFee = {
feePerTx: transactionInfo.fee,
feeLimit: transactionInfo.feeLimit,
feePerUnit: transactionInfo.feePerByte,
};

if (stakeType === 'claim') {
txData = await prepareClaimSolTx({
from: account.descriptor,
path: account.path,
symbol: account.symbol,
selectedBlockchain,
});
}
const txData = await getTransactionData(
formValues,
selectedAccount,
blockchain,
estimatedFee,
);

if (!txData) {
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ const getInfoRowsData = (
case 'ethereum':
return {
payoutDays: (
<>
~
<Translation id="TR_STAKE_DAYS" values={{ count: daysToAddToPool }} />
</>
<Translation
id="TR_STAKE_APPROXIMATE_DAYS"
values={{ count: daysToAddToPool }}
/>
),
rewardsPeriodHeading: <Translation id="TR_STAKE_ENTER_THE_STAKING_POOL" />,
rewardsPeriodSubheading: (
Expand Down
4 changes: 4 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8940,6 +8940,10 @@ export default defineMessages({
id: 'TR_STAKE_DAYS',
defaultMessage: '{count, plural, one {# day} other {# days}}',
},
TR_STAKE_APPROXIMATE_DAYS: {
id: 'TR_STAKE_APPROXIMATE_DAYS',
defaultMessage: '~{count, plural, one {# day} other {# days}}',
},
TR_STAKE_MAX_REWARD_DAYS: {
id: 'TR_STAKE_MAX_REWARD_DAYS',
defaultMessage: 'Max {count, plural, one {# day} other {# days}}',
Expand Down
41 changes: 37 additions & 4 deletions packages/suite/src/utils/suite/solanaStaking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import {
} from '@solana/web3.js';

import { NetworkSymbol } from '@suite-common/wallet-config';
import { WALLET_SDK_SOURCE } from '@suite-common/wallet-constants';
import {
SOL_COMPUTE_UNIT_LIMIT,
SOL_COMPUTE_UNIT_PRICE,
WALLET_SDK_SOURCE,
} from '@suite-common/wallet-constants';
import { Blockchain } from '@suite-common/wallet-types';
import {
networkAmountToSmallestUnit,
selectSolanaWalletSdkNetwork,
} from '@suite-common/wallet-utils';
import { Fee } from '@trezor/blockchain-link-types/src/blockbook';
import type { SolanaSignTransaction } from '@trezor/connect';

type SolanaTx = SolanaSignTransaction & {
Expand Down Expand Up @@ -83,6 +88,7 @@ interface PrepareStakeSolTxParams {
amount: string;
symbol: NetworkSymbol;
selectedBlockchain: Blockchain;
estimatedFee?: Fee[number];
}
export type PrepareStakeSolTxResponse =
| {
Expand All @@ -100,18 +106,41 @@ function isCompilableTransactionMessage(
return (tx as CompilableTransactionMessage).feePayer !== undefined;
}

type PriorityFees = {
computeUnitPrice: bigint;
computeUnitLimit: number;
};

export const dummyPriorityFeesForFeeEstimation: PriorityFees = {
computeUnitPrice: BigInt(SOL_COMPUTE_UNIT_PRICE),
computeUnitLimit: SOL_COMPUTE_UNIT_LIMIT,
};

const getStakingParams = (estimatedFee?: Fee[number]) => {
if (!estimatedFee || !estimatedFee.feePerUnit || !estimatedFee.feeLimit) {
return dummyPriorityFeesForFeeEstimation;
}

return {
сomputeUnitPrice: BigInt(estimatedFee.feePerUnit),
computeUnitLimit: Number(estimatedFee.feeLimit), // solana package expects number
};
};

export const prepareStakeSolTx = async ({
from,
path,
amount,
symbol,
selectedBlockchain,
estimatedFee,
}: PrepareStakeSolTxParams): Promise<PrepareStakeSolTxResponse> => {
try {
const solanaClient = selectSolanaWalletSdkNetwork(symbol, selectedBlockchain.url);

const lamports = networkAmountToSmallestUnit(amount, symbol);
const tx = await solanaClient.stake(from, BigInt(lamports), WALLET_SDK_SOURCE);
const params = getStakingParams(estimatedFee);
const tx = await solanaClient.stake(from, BigInt(lamports), WALLET_SDK_SOURCE, params);
const { stakeTx } = tx.result;

if (!isCompilableTransactionMessage(stakeTx)) {
Expand Down Expand Up @@ -140,12 +169,14 @@ export const prepareUnstakeSolTx = async ({
amount,
symbol,
selectedBlockchain,
estimatedFee,
}: PrepareStakeSolTxParams): Promise<PrepareStakeSolTxResponse> => {
try {
const solanaClient = selectSolanaWalletSdkNetwork(symbol, selectedBlockchain.url);

const lamports = networkAmountToSmallestUnit(amount, symbol);
const tx = await solanaClient.unstake(from, BigInt(lamports), WALLET_SDK_SOURCE);
const params = getStakingParams(estimatedFee);
const tx = await solanaClient.unstake(from, BigInt(lamports), WALLET_SDK_SOURCE, params);
const transformedTx = transformTx(tx.result.unstakeTx, path);

return {
Expand All @@ -169,11 +200,13 @@ export const prepareClaimSolTx = async ({
path,
symbol,
selectedBlockchain,
estimatedFee,
}: PrepareClaimSolTxParams): Promise<PrepareStakeSolTxResponse> => {
try {
const solanaClient = selectSolanaWalletSdkNetwork(symbol, selectedBlockchain.url);

const tx = await solanaClient.claim(from);
const params = getStakingParams(estimatedFee);
const tx = await solanaClient.claim(from, params);
const transformedTx = transformTx(tx.result.claimTx, path);

return {
Expand Down
Loading

0 comments on commit 7670a6a

Please sign in to comment.