mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-20 00:33:07 +01:00
fix(staking): fix inconsistent Solana staking amounts between UI and device
This commit is contained in:
committed by
Jaroslav Hrách
parent
7eede71ad0
commit
e6828b4a8d
@@ -7,6 +7,7 @@ import {
|
||||
composeStakingTransaction,
|
||||
} from '@suite-common/staking/src/actions/stakeFormActions';
|
||||
import {
|
||||
SolanaTxMeta,
|
||||
prepareClaimSolTx,
|
||||
prepareStakeSolTx,
|
||||
prepareUnstakeSolTx,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
BlockchainNetworks,
|
||||
EstimatedFee,
|
||||
ExternalOutput,
|
||||
PrecomposedLevels,
|
||||
PrecomposedTransaction,
|
||||
PrecomposedTransactionFinal,
|
||||
PrepareStakeSolTxResponse,
|
||||
@@ -157,6 +159,35 @@ async function estimateFee(
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const applySolanaTxMeta = (
|
||||
composed: PrecomposedLevels,
|
||||
solanaTxMeta: SolanaTxMeta,
|
||||
): PrecomposedLevels =>
|
||||
Object.fromEntries(
|
||||
Object.entries(composed).map(([key, tx]) => {
|
||||
if (tx.type === 'error') return [key, tx];
|
||||
|
||||
const nextTx = { ...tx, solanaTxMeta };
|
||||
|
||||
if (tx.type === 'final') {
|
||||
const totalSpent = new BigNumber(solanaTxMeta.deviceAmountLamports)
|
||||
.plus(solanaTxMeta.feeIncludingRentLamports)
|
||||
.toString();
|
||||
|
||||
return [
|
||||
key,
|
||||
{
|
||||
...nextTx,
|
||||
fee: solanaTxMeta.feeIncludingRentLamports,
|
||||
totalSpent,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [key, nextTx];
|
||||
}),
|
||||
);
|
||||
|
||||
export const composeTransaction =
|
||||
(formValues: StakeFormState, formState: ComposeActionContext) =>
|
||||
async (_: Dispatch, getState: GetState) => {
|
||||
@@ -179,7 +210,7 @@ export const composeTransaction =
|
||||
const { levels } = feeInfo;
|
||||
const predefinedLevels = levels.filter(l => l.label !== 'custom');
|
||||
|
||||
return composeStakingTransaction(
|
||||
const composed = composeStakingTransaction(
|
||||
formValues,
|
||||
formState,
|
||||
predefinedLevels,
|
||||
@@ -187,6 +218,23 @@ export const composeTransaction =
|
||||
estimatedFee,
|
||||
undefined,
|
||||
);
|
||||
if (!composed) return;
|
||||
|
||||
if (estimatedFee.success && estimatedFee.payload) {
|
||||
const txDataWithFee = await getTransactionData(
|
||||
formValues,
|
||||
selectedAccount,
|
||||
blockchain,
|
||||
estimatedFee.payload,
|
||||
);
|
||||
const solanaTxMeta = txDataWithFee?.success ? txDataWithFee.solanaTxMeta : undefined;
|
||||
|
||||
if (solanaTxMeta) {
|
||||
return applySolanaTxMeta(composed, solanaTxMeta);
|
||||
}
|
||||
}
|
||||
|
||||
return composed;
|
||||
};
|
||||
|
||||
export const signTransaction =
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@suite-common/wallet-constants": "workspace:*",
|
||||
"@trezor/blockchain-link": "workspace:*",
|
||||
"@trezor/blockchain-link-types": "workspace:*",
|
||||
"@trezor/blockchain-link-utils": "workspace:*",
|
||||
"@trezor/connect": "workspace:*",
|
||||
"@trezor/env-utils": "workspace:*",
|
||||
"@trezor/type-utils": "workspace:*",
|
||||
|
||||
@@ -28,11 +28,19 @@ export interface SolNetworkConfig {
|
||||
network: Network;
|
||||
}
|
||||
|
||||
export type SolanaTxMeta = {
|
||||
deviceAmountLamports: string;
|
||||
feeLamports: string;
|
||||
rentLamports: string;
|
||||
feeIncludingRentLamports: string;
|
||||
};
|
||||
|
||||
export type StakeResponse = {
|
||||
stakeTx:
|
||||
| (CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime)
|
||||
| (Transaction & TransactionMessageWithBlockhashLifetime);
|
||||
stakeAccount: Address;
|
||||
txMeta: SolanaTxMeta;
|
||||
};
|
||||
|
||||
export type Delegations = Array<Account<StakeStateAccount, Address>>;
|
||||
@@ -40,6 +48,7 @@ export type Delegations = Array<Account<StakeStateAccount, Address>>;
|
||||
export type UnstakeResponse = {
|
||||
unstakeTx: CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime;
|
||||
unstakeAmount: bigint;
|
||||
txMeta: SolanaTxMeta;
|
||||
};
|
||||
|
||||
export type Connection = RpcFromTransport<
|
||||
@@ -50,6 +59,7 @@ export type Connection = RpcFromTransport<
|
||||
export type ClaimResponse = {
|
||||
claimTx: CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime;
|
||||
totalClaimAmount: bigint;
|
||||
txMeta: SolanaTxMeta;
|
||||
};
|
||||
|
||||
export type TransactionShim = {
|
||||
|
||||
@@ -61,7 +61,7 @@ const getStakingParams = (estimatedFee?: Fee[number]) => {
|
||||
}
|
||||
|
||||
return {
|
||||
сomputeUnitPrice: BigInt(estimatedFee.feePerUnit),
|
||||
computeUnitPrice: BigInt(estimatedFee.feePerUnit),
|
||||
computeUnitLimit: Number(estimatedFee.feeLimit), // solana package expects number
|
||||
};
|
||||
};
|
||||
@@ -97,6 +97,7 @@ export const prepareStakeSolTx = async ({
|
||||
return {
|
||||
success: true,
|
||||
tx: transformedTx,
|
||||
solanaTxMeta: tx.txMeta,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -132,6 +133,7 @@ export const prepareUnstakeSolTx = async ({
|
||||
return {
|
||||
success: true,
|
||||
tx: transformedTx,
|
||||
solanaTxMeta: tx.txMeta,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -163,6 +165,7 @@ export const prepareClaimSolTx = async ({
|
||||
return {
|
||||
success: true,
|
||||
tx: transformedTx,
|
||||
solanaTxMeta: tx.txMeta,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TransactionMessageWithBlockhashLifetime,
|
||||
address,
|
||||
appendTransactionMessageInstruction,
|
||||
compileTransactionMessage,
|
||||
createAddressWithSeed,
|
||||
createNoopSigner,
|
||||
createTransactionMessage,
|
||||
@@ -21,8 +22,12 @@ import {
|
||||
setTransactionMessageLifetimeUsingBlockhash,
|
||||
} from '@solana/kit';
|
||||
import {
|
||||
SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR,
|
||||
SET_COMPUTE_UNIT_PRICE_DISCRIMINATOR,
|
||||
getSetComputeUnitLimitInstruction,
|
||||
getSetComputeUnitLimitInstructionDataDecoder,
|
||||
getSetComputeUnitPriceInstruction,
|
||||
getSetComputeUnitPriceInstructionDataDecoder,
|
||||
} from '@solana-program/compute-budget';
|
||||
import {
|
||||
STAKE_PROGRAM_ADDRESS,
|
||||
@@ -39,6 +44,11 @@ import {
|
||||
getTransferSolInstruction,
|
||||
} from '@solana-program/system';
|
||||
|
||||
import {
|
||||
SOL_BASE_FEE,
|
||||
SOL_COMPUTE_UNIT_LIMIT,
|
||||
SOL_MICROLAMPORTS_PER_LAMPORT,
|
||||
} from '@suite-common/wallet-constants';
|
||||
import {
|
||||
STAKE_ACCOUNT_V2_SIZE,
|
||||
getDelegations,
|
||||
@@ -46,6 +56,7 @@ import {
|
||||
stakeAccountState,
|
||||
} from '@trezor/blockchain-link/src/workers/solana/utils/stakingAccounts';
|
||||
import { StakeState } from '@trezor/blockchain-link-types/src/solana';
|
||||
import { COMPUTE_BUDGET_PROGRAM_ID } from '@trezor/blockchain-link-utils/src/solana';
|
||||
import { serializeError } from '@trezor/utils';
|
||||
|
||||
import { selectSolanaWalletSdkNetwork } from '../connection';
|
||||
@@ -64,6 +75,7 @@ import {
|
||||
Connection,
|
||||
Delegations,
|
||||
Params,
|
||||
SolanaTxMeta,
|
||||
StakeParams,
|
||||
StakeResponse,
|
||||
UnstakeResponse,
|
||||
@@ -160,6 +172,59 @@ const baseTx = async (
|
||||
return transactionMessage;
|
||||
};
|
||||
|
||||
export const getFeeSummary = ({
|
||||
transactionMessage,
|
||||
}: {
|
||||
transactionMessage: CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime;
|
||||
}) => {
|
||||
const compiledMessage = compileTransactionMessage(transactionMessage);
|
||||
|
||||
const baseFeeLamports = SOL_BASE_FEE * BigInt(compiledMessage.header.numSignerAccounts);
|
||||
|
||||
let unitLimit = BigInt(SOL_COMPUTE_UNIT_LIMIT);
|
||||
let unitPriceMicroLamports = 0n;
|
||||
let isUnitLimitSet = false;
|
||||
let isUnitPriceSet = false;
|
||||
|
||||
compiledMessage.instructions.forEach(instruction => {
|
||||
if (
|
||||
compiledMessage.staticAccounts[instruction.programAddressIndex] !==
|
||||
COMPUTE_BUDGET_PROGRAM_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = instruction;
|
||||
if (!data || data.length === 0) return;
|
||||
|
||||
if (data[0] === SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR && !isUnitLimitSet) {
|
||||
const decoded = getSetComputeUnitLimitInstructionDataDecoder().decode(data);
|
||||
unitLimit = BigInt(decoded.units);
|
||||
isUnitLimitSet = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (data[0] === SET_COMPUTE_UNIT_PRICE_DISCRIMINATOR && !isUnitPriceSet) {
|
||||
const decoded = getSetComputeUnitPriceInstructionDataDecoder().decode(data);
|
||||
unitPriceMicroLamports = BigInt(decoded.microLamports);
|
||||
isUnitPriceSet = true;
|
||||
}
|
||||
});
|
||||
|
||||
const priorityFeeLamports =
|
||||
(unitPriceMicroLamports * BigInt(unitLimit) + SOL_MICROLAMPORTS_PER_LAMPORT - 1n) /
|
||||
SOL_MICROLAMPORTS_PER_LAMPORT;
|
||||
|
||||
const feeLamports = baseFeeLamports + priorityFeeLamports;
|
||||
|
||||
return {
|
||||
baseFeeLamports: baseFeeLamports.toString(10),
|
||||
priorityFeeLamports: priorityFeeLamports.toString(10),
|
||||
feeLamports: feeLamports.toString(10),
|
||||
};
|
||||
};
|
||||
|
||||
const timestampInSec = (): number => (Date.now() / 1000) | 0;
|
||||
|
||||
const isLockupInForce = (
|
||||
@@ -248,7 +313,6 @@ export const stake = async ({
|
||||
initializeStakeAccountInstruction,
|
||||
stakeAccountPublicKey,
|
||||
] = await createAccountWithSeedTx(address(sender), BigInt(lamports) + minimumRent, source);
|
||||
|
||||
const delegateInstruction = getDelegateStakeInstruction({
|
||||
stake: stakeAccountPublicKey,
|
||||
vote: validator,
|
||||
@@ -276,9 +340,21 @@ export const stake = async ({
|
||||
? await partiallySignTransactionMessageWithSigners(transactionMessage)
|
||||
: transactionMessage;
|
||||
|
||||
const feeSummary = getFeeSummary({ transactionMessage });
|
||||
const feeLamports = BigInt(feeSummary.feeLamports);
|
||||
const feeIncludingRentLamports = (feeLamports + minimumRent).toString();
|
||||
const deviceAmountLamports = (lamports + minimumRent).toString();
|
||||
const txMeta: SolanaTxMeta = {
|
||||
deviceAmountLamports,
|
||||
feeLamports: feeSummary.feeLamports,
|
||||
rentLamports: minimumRent.toString(),
|
||||
feeIncludingRentLamports,
|
||||
};
|
||||
|
||||
return {
|
||||
stakeTx: signedTransactionMessage,
|
||||
stakeAccount: stakeAccountPublicKey,
|
||||
txMeta,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
@@ -430,7 +506,17 @@ export const unstake = async ({
|
||||
throw new Error('Zero instructions');
|
||||
}
|
||||
|
||||
return { unstakeTx: transactionMessage, unstakeAmount };
|
||||
const feeSummary = getFeeSummary({ transactionMessage });
|
||||
const feeLamports = BigInt(feeSummary.feeLamports);
|
||||
const feeIncludingRentLamports = (feeLamports + minimumRent).toString();
|
||||
const txMeta: SolanaTxMeta = {
|
||||
deviceAmountLamports: unstakeAmount.toString(),
|
||||
feeLamports: feeSummary.feeLamports,
|
||||
rentLamports: minimumRent.toString(),
|
||||
feeIncludingRentLamports,
|
||||
};
|
||||
|
||||
return { unstakeTx: transactionMessage, unstakeAmount, txMeta };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Solana staking: unstaking failed - ${error instanceof Error ? error.message : serializeError(error)}`,
|
||||
@@ -488,9 +574,18 @@ export const claim = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const feeSummary = getFeeSummary({ transactionMessage });
|
||||
const txMeta: SolanaTxMeta = {
|
||||
deviceAmountLamports: totalClaimableStake.toString(),
|
||||
feeLamports: feeSummary.feeLamports,
|
||||
rentLamports: '0',
|
||||
feeIncludingRentLamports: feeSummary.feeLamports,
|
||||
};
|
||||
|
||||
return {
|
||||
claimTx: transactionMessage,
|
||||
totalClaimAmount: totalClaimableStake,
|
||||
txMeta,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
{
|
||||
"path": "../../packages/blockchain-link-types"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/blockchain-link-utils"
|
||||
},
|
||||
{ "path": "../../packages/connect" },
|
||||
{ "path": "../../packages/env-utils" },
|
||||
{ "path": "../../packages/type-utils" },
|
||||
|
||||
@@ -9,4 +9,7 @@ export const SOL_STAKING_OPERATION_FEE = new BigNumber(70_000); // 0.00007 SOL
|
||||
export const SOL_COMPUTE_UNIT_PRICE = 100000;
|
||||
export const SOL_COMPUTE_UNIT_LIMIT = 200000;
|
||||
|
||||
export const SOL_BASE_FEE = 5000n;
|
||||
export const SOL_MICROLAMPORTS_PER_LAMPORT = 1_000_000n;
|
||||
|
||||
export const SOLANA_EPOCH_DAYS = 2;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SolanaTx } from '@suite-common/staking-solana';
|
||||
import { SolanaTx, SolanaTxMeta } from '@suite-common/staking-solana';
|
||||
import { NetworkSymbol } from '@suite-common/wallet-config';
|
||||
import { Fee } from '@trezor/blockchain-link-types/src/blockbook';
|
||||
|
||||
@@ -28,6 +28,7 @@ export type PrepareStakeSolTxResponse =
|
||||
| {
|
||||
success: true;
|
||||
tx: SolanaTx;
|
||||
solanaTxMeta: SolanaTxMeta;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
|
||||
@@ -10986,6 +10986,7 @@ __metadata:
|
||||
"@suite-common/wallet-constants": "workspace:*"
|
||||
"@trezor/blockchain-link": "workspace:*"
|
||||
"@trezor/blockchain-link-types": "workspace:*"
|
||||
"@trezor/blockchain-link-utils": "workspace:*"
|
||||
"@trezor/connect": "workspace:*"
|
||||
"@trezor/env-utils": "workspace:*"
|
||||
"@trezor/type-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user