diff --git a/packages/suite/src/actions/wallet/send/sendFormSolanaActions.ts b/packages/suite/src/actions/wallet/send/sendFormSolanaActions.ts index 9b3a123574c..e446c1fe6b5 100644 --- a/packages/suite/src/actions/wallet/send/sendFormSolanaActions.ts +++ b/packages/suite/src/actions/wallet/send/sendFormSolanaActions.ts @@ -24,6 +24,7 @@ import { buildTransferTransaction, buildTokenTransferTransaction, getAssociatedTokenAccountAddress, + dummyPriorityFeesForFeeEstimation, } from 'src/utils/wallet/solanaUtils'; import { SYSTEM_PROGRAM_PUBLIC_KEY } from '@trezor/blockchain-link-utils/lib/solana'; @@ -33,7 +34,9 @@ const calculate = ( feeLevel: FeeLevel, token?: TokenInfo, ): PrecomposedTransaction => { - const feeInLamports = feeLevel.feePerUnit; + const feeInLamports = feeLevel.feePerTx; + if (feeInLamports == null) throw new Error('Invalid fee.'); + let amount: string; let max: string | undefined; const availableTokenBalance = token @@ -61,7 +64,8 @@ const calculate = ( totalSpent: token ? amount : totalSpent.toString(), max, fee: feeInLamports, - feePerByte: feeInLamports, + feePerByte: feeLevel.feePerUnit, + feeLimit: feeLevel.feeLimit, token, bytes: 0, inputs: [], @@ -124,8 +128,6 @@ export const composeTransaction = const { output, decimals, tokenInfo } = composeOutputs; - let fetchedFee: string | undefined; - const { blockhash, blockHeight: lastValidBlockHeight } = selectBlockchainBlockInfoBySymbol( getState(), account.symbol, @@ -155,6 +157,7 @@ export const composeTransaction = recipientTokenAccount, blockhash, lastValidBlockHeight, + dummyPriorityFeesForFeeEstimation, ) : undefined; @@ -171,6 +174,7 @@ export const composeTransaction = formValues.outputs[0].amount || '0', blockhash, lastValidBlockHeight, + dummyPriorityFeesForFeeEstimation, ) ).compileMessage(); @@ -190,9 +194,15 @@ export const composeTransaction = }, }); + let fetchedFee: string | undefined; + let fetchedFeePerUnit: string | undefined; + let fetchedFeeLimit: string | undefined; if (estimatedFee.success) { // We access the array directly like this because the fee response from the solana worker always returns an array of size 1 - fetchedFee = estimatedFee.payload.levels[0].feePerUnit; + const feeLevel = estimatedFee.payload.levels[0]; + fetchedFee = feeLevel.feePerTx; + fetchedFeePerUnit = feeLevel.feePerUnit; + fetchedFeeLimit = feeLevel.feeLimit; } else { // Error fetching fee, fall back on default values defined in `/packages/connect/src/data/defaultFeeLevels.ts` } @@ -202,7 +212,12 @@ export const composeTransaction = // update predefined levels with fee fetched from network const predefinedLevels = levels .filter(l => l.label !== 'custom') - .map(l => ({ ...l, feePerUnit: fetchedFee || l.feePerUnit })); + .map(l => ({ + ...l, + feePerTx: fetchedFee || l.feePerTx, + feePerUnit: fetchedFeePerUnit || l.feePerUnit, + feeLimit: fetchedFeeLimit || l.feeLimit, + })); const wrappedResponse: PrecomposedLevels = {}; const response = predefinedLevels.map(level => @@ -241,7 +256,8 @@ export const signTransaction = selectedAccount.status !== 'loaded' || !device || !transactionInfo || - transactionInfo.type !== 'final' + transactionInfo.type !== 'final' || + transactionInfo.feeLimit == null ) return; @@ -278,6 +294,10 @@ export const signTransaction = recipientTokenAccounts, blockhash, lastValidBlockHeight, + { + computeUnitPrice: transactionInfo.feePerByte, + computeUnitLimit: transactionInfo.feeLimit, + }, ) : undefined; @@ -291,6 +311,10 @@ export const signTransaction = formValues.outputs[0].amount, blockhash, lastValidBlockHeight, + { + computeUnitPrice: transactionInfo.feePerByte, + computeUnitLimit: transactionInfo.feeLimit, + }, ); const serializedTx = tx.serializeMessage().toString('hex'); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx index e7ec9815f72..6989f6fae32 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx @@ -1,6 +1,6 @@ import styled, { useTheme } from 'styled-components'; import BigNumber from 'bignumber.js'; -import { getFeeUnits, formatNetworkAmount, formatAmount } from '@suite-common/wallet-utils'; +import { getFeeUnits, formatNetworkAmount, formatAmount, getFee } from '@suite-common/wallet-utils'; import { Icon, CoinLogo, variables } from '@trezor/components'; import { formatDuration, isFeatureFlagEnabled } from '@suite-common/suite-utils'; import { borders, spacingsPx, typography } from '@trezor/theme'; @@ -220,7 +220,7 @@ export const TransactionReviewSummary = ({ const theme = useTheme(); const { symbol, accountType, index } = account; - const { feePerByte } = tx; + const fee = getFee(network.networkType, tx); const spentWithoutFee = !tx.token ? new BigNumber(tx.totalSpent).minus(tx.fee).toString() : ''; const amount = !tx.token @@ -229,7 +229,7 @@ export const TransactionReviewSummary = ({ const formFeeRate = drafts[currentAccountKey]?.feePerUnit; const isFeeCustom = drafts[currentAccountKey]?.selectedFee === 'custom'; - const isComposedFeeRateDifferent = isFeeCustom && formFeeRate !== feePerByte; + const isComposedFeeRateDifferent = isFeeCustom && formFeeRate !== fee; return ( @@ -278,7 +278,7 @@ export const TransactionReviewSummary = ({ )} - {!!tx.feeLimit && ( + {!!tx.feeLimit && network.networkType !== 'solana' && ( @@ -300,7 +300,7 @@ export const TransactionReviewSummary = ({ - {feePerByte} {getFeeUnits(network.networkType)} + {fee} {getFeeUnits(network.networkType)} diff --git a/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts b/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts index 32171604e1f..ec7901bb397 100644 --- a/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts +++ b/packages/suite/src/hooks/wallet/__fixtures__/useSendForm.ts @@ -163,11 +163,19 @@ const DEFAULT_FEES = { levels: [{ label: 'normal', feePerUnit: '12', blocks: -1 }], }, sol: { - minFee: 5000, - maxFee: 5000, - blockHeight: 1, - blockTime: 1, - levels: [{ label: 'normal', feePerUnit: '5000', blocks: -1 }], + minFee: -1, + maxFee: -1, + blockHeight: -1, + blockTime: -1, + levels: [ + { + label: 'normal', + feePerUnit: '100000', + feeLimit: '50000', + feePerTx: '10000', + blocks: -1, + }, + ], }, }; @@ -1204,13 +1212,13 @@ export const setMax = [ composedLevels: { normal: { type: 'final', - fee: '5000', + fee: '10000', totalSpent: '10000000000', }, custom: undefined, }, formValues: { - outputs: [{ amount: '9.999995' }], + outputs: [{ amount: '9.99999' }], }, }, }, diff --git a/packages/suite/src/utils/wallet/__fixtures__/solanaUtils.ts b/packages/suite/src/utils/wallet/__fixtures__/solanaUtils.ts index 259b3db4665..70e3d1e1240 100644 --- a/packages/suite/src/utils/wallet/__fixtures__/solanaUtils.ts +++ b/packages/suite/src/utils/wallet/__fixtures__/solanaUtils.ts @@ -192,9 +192,13 @@ export const fixtures = { toTokenAccount: undefined, blockhash: '7xpT7BDE7q1ZWhe6Pg8PHRYbqgDwNK3L2v97rEfsjMkn', lastValidBlockHeight: 50, + priorityFees: { + computeUnitPrice: '100000', + computeUnitLimit: '50000', + }, }, expectedOutput: - '01000508c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc580000000000000000000000000000000000000000000000000000000000000000527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f28c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859d27c181cb023db6239e22e49e4b67f7dd9ed13f3d7ed319f9e91b3bc64cec0a906ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb020506000206040307000704010402000a0c00e1f5050000000009', + '01000609c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc580000000000000000000000000000000000000000000000000000000000000000527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f28c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8590306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000d27c181cb023db6239e22e49e4b67f7dd9ed13f3d7ed319f9e91b3bc64cec0a906ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb040600050250c3000006000903a0860100000000000506000207040308000804010402000a0c00e1f5050000000009', }, { description: @@ -218,9 +222,13 @@ export const fixtures = { }, blockhash: '7xpT7BDE7q1ZWhe6Pg8PHRYbqgDwNK3L2v97rEfsjMkn', lastValidBlockHeight: 50, + priorityFees: { + computeUnitPrice: '100000', + computeUnitLimit: '50000', + }, }, expectedOutput: - '01000205c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc58527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f206ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb010404010302000a0c00e1f5050000000009', + '01000306c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc58527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f20306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a4000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb030400050250c3000004000903a0860100000000000504010302000a0c00e1f5050000000009', }, { description: @@ -241,9 +249,13 @@ export const fixtures = { toTokenAccount: undefined, blockhash: '7xpT7BDE7q1ZWhe6Pg8PHRYbqgDwNK3L2v97rEfsjMkn', lastValidBlockHeight: 50, + priorityFees: { + computeUnitPrice: '100000', + computeUnitLimit: '50000', + }, }, expectedOutput: - '01000205c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc58527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f206ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb010404010302000a0c00e1f5050000000009', + '01000306c80f8b50107e9f3e3c16a661b8c806df454a6deb293d5e8730a9d28f2f4998c6a99c9c4d0c7def9dd60a3a40dc5266faf41996310aa62ad6cbd9b64e1e2cca78ebaa24826cef9644c1ecf0dfcf955775b8438528e97820efc2b20ed46be1dc58527706a12f3f7c3c852582f0f79b515c03c6ffbe6e3100044ba7c982eb5cf9f20306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a4000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a96772b7d36a2e66e52c817f385d7e94d3d4b6d47d7171c9f2dd86c6f1be1a93eb030400050250c3000004000903a0860100000000000504010302000a0c00e1f5050000000009', }, ], }; diff --git a/packages/suite/src/utils/wallet/__tests__/solanaUtils.test.ts b/packages/suite/src/utils/wallet/__tests__/solanaUtils.test.ts index 1f340b843ef..89684de2d0c 100644 --- a/packages/suite/src/utils/wallet/__tests__/solanaUtils.test.ts +++ b/packages/suite/src/utils/wallet/__tests__/solanaUtils.test.ts @@ -76,6 +76,7 @@ describe('solana utils', () => { input.toTokenAccount, input.blockhash, input.lastValidBlockHeight, + input.priorityFees, ); const message = tx.transaction.compileMessage().serialize().toString('hex'); diff --git a/packages/suite/src/utils/wallet/solanaUtils.ts b/packages/suite/src/utils/wallet/solanaUtils.ts index b8dfdf27564..d0544f250d2 100644 --- a/packages/suite/src/utils/wallet/solanaUtils.ts +++ b/packages/suite/src/utils/wallet/solanaUtils.ts @@ -42,19 +42,43 @@ const encodeTokenTransferInstructionData = (instruction: { return b.subarray(0, span); }; +type PriorityFees = { computeUnitPrice: string; computeUnitLimit: string }; + +export const dummyPriorityFeesForFeeEstimation: PriorityFees = { + computeUnitPrice: '100000', + computeUnitLimit: '200000', +}; + +const addPriorityFees = async (transaction: Transaction, priorityFees: PriorityFees) => { + const { ComputeBudgetProgram } = await loadSolanaLib(); + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: parseInt(priorityFees.computeUnitLimit, 10), + }), + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: parseInt(priorityFees.computeUnitPrice, 10), + }), + ); +}; + export const buildTransferTransaction = async ( fromAddress: string, toAddress: string, amountInSol: string, blockhash: string, lastValidBlockHeight: number, + priorityFees: PriorityFees, ) => { const { Transaction, SystemProgram, PublicKey } = await loadSolanaLib(); const transaction = new Transaction({ blockhash, lastValidBlockHeight, feePayer: new PublicKey(fromAddress), - }).add( + }); + + await addPriorityFees(transaction, priorityFees); + + transaction.add( SystemProgram.transfer({ fromPubkey: new PublicKey(fromAddress), toPubkey: new PublicKey(toAddress), @@ -210,6 +234,7 @@ export const buildTokenTransferTransaction = async ( toTokenAccount: TokenAccount | undefined, blockhash: string, lastValidBlockHeight: number, + priorityFees: PriorityFees, ): Promise => { const { Transaction, PublicKey } = await loadSolanaLib(); @@ -219,6 +244,8 @@ export const buildTokenTransferTransaction = async ( feePayer: new PublicKey(fromAddress), }); + await addPriorityFees(transaction, priorityFees); + // Token transaction building logic const tokenAmount = new BigNumber(tokenUiAmount).times(10 ** tokenDecimals); diff --git a/suite-common/wallet-utils/src/sendFormUtils.ts b/suite-common/wallet-utils/src/sendFormUtils.ts index af6e11dfa14..d47d197593c 100644 --- a/suite-common/wallet-utils/src/sendFormUtils.ts +++ b/suite-common/wallet-utils/src/sendFormUtils.ts @@ -31,6 +31,8 @@ import type { Account, CurrencyOption, ExcludedUtxos, + PrecomposedTransactionFinal, + TxFinalCardano, } from '@suite-common/wallet-types'; import { amountToSatoshi, getUtxoOutpoint, networkAmountToSatoshi } from './accountUtils'; @@ -201,6 +203,15 @@ export const getFeeUnits = (networkType: NetworkType) => { return 'sat/B'; }; +export const getFee = ( + networkType: NetworkType, + tx: PrecomposedTransactionFinal | TxFinalCardano, +) => { + if (networkType === 'solana') return tx.fee; + + return tx.feePerByte; +}; + // Find all validation errors set while composing a transaction export const findComposeErrors = ( errors: FieldErrors,