diff --git a/packages/product-components/src/components/FeeRate/FeeRate.tsx b/packages/product-components/src/components/FeeRate/FeeRate.tsx
index 831077754c3..f4456368cd9 100644
--- a/packages/product-components/src/components/FeeRate/FeeRate.tsx
+++ b/packages/product-components/src/components/FeeRate/FeeRate.tsx
@@ -1,21 +1,38 @@
-import { NetworkType } from '@suite-common/wallet-config';
+import { NetworkSymbol, NetworkType, hasNetworkSettlementLayer } from '@suite-common/wallet-config';
import { getFeeUnits } from '@suite-common/wallet-utils';
import { BigNumber } from '@trezor/utils';
type FeeRateProps = {
feeRate?: string | BigNumber;
networkType: NetworkType;
+ symbol: NetworkSymbol;
};
-export const FeeRate = ({ feeRate, networkType }: FeeRateProps) => {
+export const FeeRate = ({ feeRate, networkType, symbol }: FeeRateProps) => {
if (!feeRate) return null;
- const fee = typeof feeRate === 'string' ? new BigNumber(feeRate) : feeRate;
+ const fee = (() => {
+ switch (networkType) {
+ case 'ethereum': {
+ const decimals = hasNetworkSettlementLayer(symbol) ? 4 : 2;
+ const multiplier = Math.pow(10, decimals);
+ const value = Math.ceil(Number(feeRate) * multiplier) / multiplier;
+
+ return value.toFixed(decimals);
+ }
+ case 'bitcoin': {
+ const feeBn = typeof feeRate === 'string' ? new BigNumber(feeRate) : feeRate;
+
+ return feeBn.toFixed(2);
+ }
+ default:
+ return typeof feeRate === 'string' ? feeRate : feeRate.toString();
+ }
+ })();
return (
- {networkType === 'bitcoin' ? fee.toFixed(2) : fee.toString()}
- {getFeeUnits(networkType)}
+ {fee} {getFeeUnits(networkType)}
);
};
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 5eee69b5801..eecc9cc1e12 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx
@@ -46,9 +46,8 @@ export const TransactionReviewSummary = ({
) as string;
const fees = useSelector(state => state.wallet.fees);
const locale = useLocales();
-
- const network = networks[account.symbol];
const { symbol, accountType, index, networkType } = account;
+ const network = networks[symbol];
const fee = getFee(networkType, tx);
const estimateTime = getEstimatedTime(networkType, fees[account.symbol], tx);
@@ -87,11 +86,11 @@ export const TransactionReviewSummary = ({
{': '}
-
+
) : (
-
+
)}
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx
index 62ca5784ef2..7d0901a38cf 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx
@@ -151,6 +151,7 @@ export const BasicTxDetails = ({
)}
@@ -174,6 +175,7 @@ export const BasicTxDetails = ({
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransaction.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransaction.tsx
index 7c9187d160a..20b8f60b58f 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransaction.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransaction.tsx
@@ -68,7 +68,11 @@ export const CancelTransaction = ({ tx, selectedAccount }: CancelTransactionProp
-
+
}
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx
index 225d33191e0..76493d739b4 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx
@@ -30,7 +30,7 @@ const ChangeFeeLoaded = (props: ChangeFeeProps) => {
const feeRate =
networkType === 'bitcoin' && tx.rbfParams?.feeRate !== undefined ? (
-
+
) : null;
const fee = formatNetworkAmount(tx.fee, tx.symbol);
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee.tsx
deleted file mode 100644
index 660d3302215..00000000000
--- a/packages/suite/src/components/wallet/Fees/CustomFee.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import {
- Control,
- FieldErrors,
- FieldPath,
- UseFormGetValues,
- UseFormRegister,
- UseFormReturn,
- UseFormSetValue,
-} from 'react-hook-form';
-
-import { NetworkType } from '@suite-common/wallet-config';
-import { FeeInfo, FormState } from '@suite-common/wallet-types';
-import { getFeeUnits, getInputState, isInteger } from '@suite-common/wallet-utils';
-import {
- Banner,
- Column,
- Grid,
- Icon,
- Note,
- Row,
- Text,
- useMediaQuery,
- variables,
-} from '@trezor/components';
-import { FeeLevel } from '@trezor/connect';
-import { NumberInput } from '@trezor/product-components';
-import { spacings } from '@trezor/theme';
-import { HELP_CENTER_TRANSACTION_FEES_URL } from '@trezor/urls';
-import { BigNumber } from '@trezor/utils/src/bigNumber';
-
-import { Translation } from 'src/components/suite';
-import { LearnMoreButton } from 'src/components/suite/LearnMoreButton';
-import { useSelector, useTranslation } from 'src/hooks/suite';
-import { selectLanguage } from 'src/reducers/suite/suiteReducer';
-import { validateDecimals } from 'src/utils/suite/validation';
-
-import { InputError } from '../InputError';
-
-const FEE_PER_UNIT = 'feePerUnit';
-const FEE_LIMIT = 'feeLimit';
-
-interface CustomFeeProps {
- networkType: NetworkType;
- feeInfo: FeeInfo;
- errors: FieldErrors;
- register: UseFormRegister;
- control: Control;
- setValue: UseFormSetValue;
- getValues: UseFormGetValues;
- composedFeePerByte: string;
-}
-
-// TODO: revisit with priority fees
-const getCurrentFee = (levels: FeeLevel[]) => {
- const middleIndex = Math.floor((levels.length - 1) / 2);
-
- return levels[middleIndex].feePerUnit;
-};
-
-export const CustomFee = ({
- networkType,
- feeInfo,
- register,
- control,
- composedFeePerByte,
- ...props
-}: CustomFeeProps) => {
- const { translationString } = useTranslation();
- const isBelowLaptop = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.LG})`);
-
- const locale = useSelector(selectLanguage);
-
- // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072.
- const { getValues, setValue } = props as unknown as UseFormReturn;
- const errors = props.errors as unknown as FieldErrors;
-
- const { maxFee, minFee } = feeInfo;
-
- const feePerUnitValue = getValues(FEE_PER_UNIT);
- const feeUnits = getFeeUnits(networkType);
- const estimatedFeeLimit = getValues('estimatedFeeLimit');
-
- const feePerUnitError = errors.feePerUnit;
- const feeLimitError = errors.feeLimit;
-
- const useFeeLimit = networkType === 'ethereum';
- const isComposedFeeRateDifferent =
- !feePerUnitError && composedFeePerByte && feePerUnitValue !== composedFeePerByte;
- let feeDifferenceWarning;
- if (isComposedFeeRateDifferent && networkType === 'bitcoin') {
- const baseFee = getValues('baseFee');
- feeDifferenceWarning = (
-
- {composedFeePerByte} {feeUnits}
- >
- ),
- }}
- />
- );
- }
-
- const sharedRules = {
- required: translationString('CUSTOM_FEE_IS_NOT_SET'),
- // Allow decimals in ETH since GWEI is not a satoshi.
- validate: (value: string) => {
- if (['bitcoin', 'ethereum'].includes(networkType) && !isInteger(value)) {
- return translationString('CUSTOM_FEE_IS_NOT_INTEGER');
- }
- },
- };
- const feeLimitRules = {
- ...sharedRules,
- validate: {
- ...sharedRules.validate,
- feeLimit: (value: string) => {
- const feeBig = new BigNumber(value);
- if (estimatedFeeLimit && feeBig.lt(estimatedFeeLimit)) {
- return translationString('CUSTOM_FEE_LIMIT_BELOW_RECOMMENDED');
- }
- },
- },
- };
- const feeRules = {
- ...sharedRules,
- validate: {
- ...sharedRules.validate,
- bitcoinDecimalsLimit: validateDecimals(translationString, {
- decimals: 2,
- except: networkType !== 'bitcoin',
- }),
- // GWEI: 9 decimal places.
- ethereumDecimalsLimit: validateDecimals(translationString, {
- decimals: 9,
- except: networkType !== 'ethereum',
- }),
- range: (value: string) => {
- const customFee = new BigNumber(value);
-
- if (customFee.isGreaterThan(maxFee) || customFee.isLessThan(minFee)) {
- return translationString('CUSTOM_FEE_NOT_IN_RANGE', {
- minFee: new BigNumber(minFee).toString(),
- maxFee: new BigNumber(maxFee).toString(),
- });
- }
- },
- },
- };
-
- const feeLimitValidationProps = {
- onClick: () =>
- estimatedFeeLimit &&
- setValue(FEE_LIMIT, estimatedFeeLimit, {
- shouldValidate: true,
- }),
- text: translationString('CUSTOM_FEE_LIMIT_USE_RECOMMENDED'),
- };
- const validationButtonProps =
- feeLimitError?.type === 'feeLimit' ? feeLimitValidationProps : undefined;
-
- return (
-
-
- }
- >
-
-
-
-
-
-
-
-
-
- {getCurrentFee(feeInfo.levels)} {getFeeUnits(networkType)}
-
-
-
-
-
-
- {useFeeLimit ? (
- }
- locale={locale}
- control={control}
- inputState={getInputState(feeLimitError)}
- name={FEE_LIMIT}
- data-testid={FEE_LIMIT}
- bottomText={
- feeLimitError?.message ? (
-
- ) : null
- }
- rules={feeLimitRules}
- />
- ) : (
- )} />
- )}
- : undefined}
- locale={locale}
- control={control}
- inputState={getInputState(feePerUnitError)}
- innerAddon={
-
- {feeUnits}
-
- }
- name={FEE_PER_UNIT}
- data-testid={FEE_PER_UNIT}
- rules={feeRules}
- bottomText={feePerUnitError?.message || null}
- />
-
- {feeDifferenceWarning && {feeDifferenceWarning}}
-
- );
-};
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx
new file mode 100644
index 00000000000..9013f7de8eb
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx
@@ -0,0 +1,29 @@
+import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
+import { Icon, IconName, Row, Text } from '@trezor/components';
+import { FeeRate } from '@trezor/product-components';
+import { spacings } from '@trezor/theme';
+
+import { Translation } from 'src/components/suite';
+
+type CurrentFeeProps = {
+ networkType: NetworkType;
+ feeIconName: IconName;
+ currentFee: string;
+ symbol: NetworkSymbol;
+};
+
+export const CurrentFee = ({ networkType, feeIconName, currentFee, symbol }: CurrentFeeProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx
new file mode 100644
index 00000000000..3d00bfa4dd6
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx
@@ -0,0 +1,125 @@
+import {
+ Control,
+ FieldErrors,
+ UseFormGetValues,
+ UseFormRegister,
+ UseFormSetValue,
+} from 'react-hook-form';
+
+import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
+import { FeeInfo, FormState } from '@suite-common/wallet-types';
+import { getFeeUnits, isInteger } from '@suite-common/wallet-utils';
+
+import { useSelector, useTranslation } from 'src/hooks/suite';
+import { TranslationFunction } from 'src/hooks/suite/useTranslation';
+import { selectLanguage } from 'src/reducers/suite/suiteReducer';
+
+import { CurrentFee } from './CurrentFee';
+import { CustomFeeEthereum } from './CustomFeeEthereum';
+import { CustomFeeMisc } from './CustomFeeMisc';
+import { CustomFeeWrapper } from './CustomFeeWrapper';
+
+export const FEE_PER_UNIT = 'feePerUnit';
+export const FEE_LIMIT = 'feeLimit';
+
+export type CustomFeeBasicProps = {
+ networkType: NetworkType;
+ feeInfo: FeeInfo;
+ errors: FieldErrors;
+ register: UseFormRegister;
+ control: Control;
+ setValue: UseFormSetValue;
+ getValues: UseFormGetValues;
+ composedFeePerByte: string;
+ locale: string;
+ translationString: TranslationFunction;
+ feeUnits: string;
+ sharedRules: {
+ required: string;
+ validate: (value: string) => string | undefined;
+ };
+};
+
+interface CustomFeeProps {
+ networkType: NetworkType;
+ symbol: NetworkSymbol;
+ feeInfo: FeeInfo;
+ errors: FieldErrors;
+ register: UseFormRegister;
+ control: Control;
+ setValue: UseFormSetValue;
+ getValues: UseFormGetValues;
+ composedFeePerByte: string;
+}
+
+export const CustomFee = ({
+ networkType,
+ symbol,
+ feeInfo,
+ register,
+ control,
+ composedFeePerByte,
+ ...props
+}: CustomFeeProps) => {
+ const { translationString } = useTranslation();
+
+ const sharedRules = {
+ required: translationString('CUSTOM_FEE_IS_NOT_SET'),
+ // Allow decimals in ETH since GWEI is not a satoshi.
+ validate: (value: string) => {
+ if (['bitcoin', 'ethereum'].includes(networkType) && !isInteger(value)) {
+ return translationString('CUSTOM_FEE_IS_NOT_INTEGER');
+ }
+ },
+ };
+
+ const locale = useSelector(selectLanguage);
+
+ const getCurrentFee = () => {
+ const { levels } = feeInfo;
+ const middleIndex = Math.floor((levels.length - 1) / 2);
+
+ return levels[middleIndex].feePerUnit;
+ };
+
+ const feeIconName = networkType === 'ethereum' ? 'gasPump' : 'receipt';
+ const feeUnits = getFeeUnits(networkType);
+
+ return (
+
+
+ {networkType === 'ethereum' ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeEthereum.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeEthereum.tsx
new file mode 100644
index 00000000000..bd01851b63b
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeEthereum.tsx
@@ -0,0 +1,125 @@
+import { FieldErrors, UseFormReturn } from 'react-hook-form';
+
+import { FormState } from '@suite-common/wallet-types';
+import { getInputState } from '@suite-common/wallet-utils';
+import { Text } from '@trezor/components';
+import { NumberInput } from '@trezor/product-components';
+import { BigNumber } from '@trezor/utils/src/bigNumber';
+
+import { Translation } from 'src/components/suite';
+import { InputError } from 'src/components/wallet';
+import { validateDecimals } from 'src/utils/suite/validation';
+
+import { CustomFeeBasicProps, FEE_LIMIT, FEE_PER_UNIT } from './CustomFee';
+
+export const CustomFeeEthereum = ({
+ networkType,
+ feeInfo,
+ control,
+ locale,
+ translationString,
+ feeUnits,
+ sharedRules,
+ ...props
+}: CustomFeeBasicProps) => {
+ // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072.
+ const { getValues, setValue } = props as unknown as UseFormReturn;
+ const errors = props.errors as unknown as FieldErrors;
+
+ const { maxFee, minFee } = feeInfo;
+
+ const estimatedFeeLimit = getValues('estimatedFeeLimit');
+ const feePerUnitError = errors.feePerUnit;
+ const feeLimitError = errors.feeLimit;
+
+ const feeLimitRules = {
+ required: translationString('GAS_LIMIT_IS_NOT_SET'),
+ validate: {
+ ...sharedRules.validate,
+ feeLimit: (value: string) => {
+ const feeBig = new BigNumber(value);
+ if (estimatedFeeLimit && feeBig.lt(estimatedFeeLimit)) {
+ return translationString('CUSTOM_FEE_LIMIT_BELOW_RECOMMENDED');
+ }
+ },
+ },
+ };
+ const feeRules = {
+ ...sharedRules,
+ validate: {
+ ...sharedRules.validate,
+ // GWEI: 9 decimal places.
+ ethereumDecimalsLimit: validateDecimals(translationString, {
+ decimals: 9,
+ except: networkType !== 'ethereum',
+ }),
+ range: (value: string) => {
+ const customFee = new BigNumber(value);
+
+ if (customFee.isGreaterThan(maxFee) || customFee.isLessThan(minFee)) {
+ return translationString('CUSTOM_FEE_NOT_IN_RANGE', {
+ minFee: new BigNumber(minFee).toString(),
+ maxFee: new BigNumber(maxFee).toString(),
+ });
+ }
+ },
+ },
+ };
+
+ const feeLimitValidationProps = {
+ onClick: () =>
+ estimatedFeeLimit &&
+ setValue(FEE_LIMIT, estimatedFeeLimit, {
+ shouldValidate: true,
+ }),
+ text: translationString('CUSTOM_FEE_LIMIT_USE_RECOMMENDED'),
+ };
+
+ const feeLimitValidationButtonProps =
+ feeLimitError?.type === 'feeLimit' ? feeLimitValidationProps : undefined;
+
+ const gasLimitInput = (
+ }
+ locale={locale}
+ control={control}
+ inputState={getInputState(feeLimitError)}
+ name={FEE_LIMIT}
+ data-testid={FEE_LIMIT}
+ bottomText={
+ feeLimitError?.message ? (
+
+ ) : null
+ }
+ rules={feeLimitRules}
+ />
+ );
+
+ const legacyEvmInputFields = (
+ }
+ locale={locale}
+ control={control}
+ inputState={getInputState(feePerUnitError)}
+ innerAddon={
+
+ {feeUnits}
+
+ }
+ name={FEE_PER_UNIT}
+ data-testid={FEE_PER_UNIT}
+ rules={feeRules}
+ bottomText={feePerUnitError?.message || null}
+ />
+ );
+
+ return (
+ <>
+ {gasLimitInput}
+ {legacyEvmInputFields}
+ >
+ );
+};
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeMisc.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeMisc.tsx
new file mode 100644
index 00000000000..0199f216f7f
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeMisc.tsx
@@ -0,0 +1,96 @@
+import { FieldErrors, FieldPath, UseFormReturn } from 'react-hook-form';
+
+import { FormState } from '@suite-common/wallet-types';
+import { getInputState } from '@suite-common/wallet-utils';
+import { Note, Text } from '@trezor/components';
+import { NumberInput } from '@trezor/product-components';
+import { BigNumber } from '@trezor/utils/src/bigNumber';
+
+import { Translation } from 'src/components/suite';
+import { validateDecimals } from 'src/utils/suite/validation';
+
+import { CustomFeeBasicProps, FEE_LIMIT, FEE_PER_UNIT } from './CustomFee';
+
+export const CustomFeeMisc = ({
+ networkType,
+ feeInfo,
+ register,
+ control,
+ composedFeePerByte,
+ locale,
+ translationString,
+ feeUnits,
+ sharedRules,
+ ...props
+}: CustomFeeBasicProps) => {
+ // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072.
+ const { getValues } = props as unknown as UseFormReturn;
+ const errors = props.errors as unknown as FieldErrors;
+
+ const { maxFee, minFee } = feeInfo;
+
+ const feePerUnitValue = getValues(FEE_PER_UNIT);
+ const feePerUnitError = errors.feePerUnit;
+
+ const isComposedFeeRateDifferent =
+ !feePerUnitError && composedFeePerByte && feePerUnitValue !== composedFeePerByte;
+
+ let feeDifferenceWarning;
+ if (isComposedFeeRateDifferent && networkType === 'bitcoin') {
+ const baseFee = getValues('baseFee');
+ feeDifferenceWarning = (
+
+ {composedFeePerByte} {feeUnits}
+ >
+ ),
+ }}
+ />
+ );
+ }
+
+ const feeRules = {
+ ...sharedRules,
+ validate: {
+ ...sharedRules.validate,
+ bitcoinDecimalsLimit: validateDecimals(translationString, {
+ decimals: 2,
+ except: networkType !== 'bitcoin',
+ }),
+ range: (value: string) => {
+ const customFee = new BigNumber(value);
+
+ if (customFee.isGreaterThan(maxFee) || customFee.isLessThan(minFee)) {
+ return translationString('CUSTOM_FEE_NOT_IN_RANGE', {
+ minFee: new BigNumber(minFee).toString(),
+ maxFee: new BigNumber(maxFee).toString(),
+ });
+ }
+ },
+ },
+ };
+
+ return (
+ <>
+ )} />
+
+ {feeUnits}
+
+ }
+ name={FEE_PER_UNIT}
+ data-testid={FEE_PER_UNIT}
+ rules={feeRules}
+ bottomText={feePerUnitError?.message || null}
+ />
+ {feeDifferenceWarning && {feeDifferenceWarning}}
+ >
+ );
+};
diff --git a/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeWrapper.tsx b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeWrapper.tsx
new file mode 100644
index 00000000000..03686713e16
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/CustomFee/CustomFeeWrapper.tsx
@@ -0,0 +1,29 @@
+import { Banner, Column } from '@trezor/components';
+import { spacings } from '@trezor/theme';
+import { HELP_CENTER_TRANSACTION_FEES_URL } from '@trezor/urls';
+
+import { Translation } from 'src/components/suite';
+import { LearnMoreButton } from 'src/components/suite/LearnMoreButton';
+
+interface CustomFeeWrapperProps {
+ children: React.ReactNode;
+}
+
+export const CustomFeeWrapper = ({ children }: CustomFeeWrapperProps) => (
+
+
+ }
+ >
+
+
+ {children}
+
+);
diff --git a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx b/packages/suite/src/components/wallet/Fees/FeeDetails.tsx
deleted file mode 100644
index eded9a44fe2..00000000000
--- a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx
+++ /dev/null
@@ -1,291 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-
-import styled from 'styled-components';
-
-import { formatDurationStrict } from '@suite-common/suite-utils';
-import { NetworkSymbol, NetworkType, getNetwork } from '@suite-common/wallet-config';
-import {
- FeeInfo,
- PrecomposedTransaction,
- PrecomposedTransactionCardano,
-} from '@suite-common/wallet-types';
-import { getFeeUnits } from '@suite-common/wallet-utils';
-import { Box, Column, ElevationUp, RadioCard, Row, Text } from '@trezor/components';
-import { FeeLevel } from '@trezor/connect';
-import { FeeRate } from '@trezor/product-components';
-import { spacings, spacingsPx } from '@trezor/theme';
-
-import { FiatValue } from 'src/components/suite/FiatValue';
-import { useLocales } from 'src/hooks/suite';
-
-import { FeeOption } from './Fees';
-
-type DetailsProps = {
- networkType: NetworkType;
- symbol: NetworkSymbol;
- selectedLevel: FeeLevel;
- // fields below are validated as false-positives, eslint claims that they are not used...
- feeOptions: FeeOption[];
- feeInfo: FeeInfo;
- changeFeeLevel: (level: FeeLevel['label']) => void;
- transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano;
-
- showFee: boolean;
-};
-
-export const FeeCardsWrapper = styled.div<{ $columns: number }>`
- width: 100%;
- display: grid;
- grid-template-columns: repeat(${({ $columns }) => $columns}, 1fr);
- gap: ${spacingsPx.sm};
- align-items: stretch;
-`;
-
-type FeeCardProps = {
- value: FeeLevel['label'];
- isSelected: boolean;
- changeFeeLevel: (level: FeeLevel['label']) => void;
- topLeftChild: React.ReactNode;
- topRightChild: React.ReactNode;
- bottomLeftChild: React.ReactNode;
- bottomRightChild: React.ReactNode;
-};
-
-const FEE_CARD_MIN_WIDTH = 220;
-
-type ResizeObserverType = (
- feeOptions: FeeOption[],
- setColumns: (columns: number) => void,
-) => ResizeObserver;
-
-const resizeObserver: ResizeObserverType = (feeOptions, setColumns) =>
- new ResizeObserver(entries => {
- const borderBoxSize = entries[0].borderBoxSize?.[0];
- if (!borderBoxSize) {
- return;
- }
-
- const { inlineSize: elementWidth } = borderBoxSize;
-
- const minWidth = (FEE_CARD_MIN_WIDTH + spacings.xs) * feeOptions.length;
-
- const columns = elementWidth > minWidth ? feeOptions.length : 1;
-
- setColumns(columns);
- });
-
-const FeeCard = ({
- value,
- isSelected,
- changeFeeLevel,
- topLeftChild,
- topRightChild,
- bottomLeftChild,
- bottomRightChild,
-}: FeeCardProps) => (
-
-
- changeFeeLevel(value)} isActive={isSelected}>
-
-
- {topLeftChild}
-
- {topRightChild}
-
-
-
- {bottomLeftChild}
-
- {bottomRightChild}
-
-
-
-
-
-
-);
-
-const BitcoinDetails = ({
- networkType,
- feeInfo,
- transactionInfo,
- feeOptions,
- showFee,
- selectedLevel,
- changeFeeLevel,
- symbol,
-}: DetailsProps) => {
- const [columns, setColumns] = React.useState(1);
- const locale = useLocales();
-
- const hasInfo = transactionInfo && transactionInfo.type !== 'error';
- const ref = useRef(null);
-
- useEffect(() => {
- if (!ref.current) return;
-
- resizeObserver(feeOptions, setColumns).observe(ref.current);
-
- return () => resizeObserver(feeOptions, setColumns).disconnect();
- }, [feeOptions, setColumns]);
-
- return (
- showFee && (
-
- {feeOptions?.map((fee, index) => (
- {fee.label}
- }
- topRightChild={
- <>
- ~
- {formatDurationStrict(
- feeInfo.blockTime * (fee?.blocks ?? 0) * 60,
- locale,
- )}
- >
- }
- bottomLeftChild={
-
- }
- bottomRightChild={
- <>
-
- {hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
- >
- }
- />
- ))}
-
- )
- );
-};
-
-const EthereumDetails = ({
- showFee,
- feeOptions,
- selectedLevel,
- changeFeeLevel,
- symbol,
- networkType,
-}: DetailsProps) => {
- const hasSettlementLayer = !!getNetwork(symbol).settlementLayer;
-
- // TODO: this can be probably moved to FeeRate component
- const formatFeePerUnit = (feePerUnit?: string) => {
- const num = Number(feePerUnit) || 0;
-
- const numOfDecimalPlaces = hasSettlementLayer ? 4 : 2;
-
- const multiplier = Math.pow(10, numOfDecimalPlaces);
-
- return (Math.ceil(num * multiplier) / multiplier).toFixed(numOfDecimalPlaces);
- };
-
- return (
- showFee && (
-
-
- {feeOptions?.map((fee, index) => (
- {fee.label}
- }
- topRightChild=""
- bottomLeftChild={
-
- }
- bottomRightChild={
-
- }
- />
- ))}
-
-
- )
- );
-};
-
-// Solana, Ripple, Cardano and other networks with only one option
-const MiscDetails = ({
- networkType,
- showFee,
- feeOptions,
- symbol,
- changeFeeLevel,
-}: DetailsProps) => {
- if (!feeOptions?.length) return null;
-
- const feeOption = feeOptions[0]; // in the future Solana should have it's own Details component
- const feeAmount = networkType === 'solana' ? feeOption.feePerTx : feeOption.feePerUnit;
-
- return (
- showFee && (
-
- {feeOption.label}
- }
- topRightChild=""
- bottomLeftChild={
-
- }
- bottomRightChild={
-
- {feeAmount} {getFeeUnits(networkType)}
-
- }
- />
-
- )
- );
-};
-
-export const FeeDetails = (props: DetailsProps) => {
- const { networkType } = props;
-
- const detailsComponentMap: Partial>> = {
- bitcoin: BitcoinDetails,
- ethereum: EthereumDetails,
- };
-
- const DetailsComponent = detailsComponentMap[networkType] ?? MiscDetails;
-
- return (
-
-
-
-
-
- );
-};
diff --git a/packages/suite/src/components/wallet/Fees/Fees.tsx b/packages/suite/src/components/wallet/Fees/Fees.tsx
index d3a28377b5b..ff77f841387 100644
--- a/packages/suite/src/components/wallet/Fees/Fees.tsx
+++ b/packages/suite/src/components/wallet/Fees/Fees.tsx
@@ -26,8 +26,8 @@ import { spacings, spacingsPx } from '@trezor/theme';
import { FiatValue, FormattedCryptoAmount, Translation } from 'src/components/suite';
import { Account } from 'src/types/wallet';
-import { CustomFee } from './CustomFee';
-import { FeeDetails } from './FeeDetails';
+import { CustomFee } from './CustomFee/CustomFee';
+import { StandardFee } from './StandardFee/StandardFee';
const FEE_LEVELS_TRANSLATIONS: Record = {
custom: 'FEE_LEVEL_ADVANCED',
@@ -37,13 +37,13 @@ const FEE_LEVELS_TRANSLATIONS: Record = {
low: 'FEE_LEVEL_LOW',
} as const;
-export type FeeOption = {
+export type FeeOptionType = {
label: React.ReactNode;
value: FeeLevel['label'];
blocks?: number;
feePerUnit?: string;
networkAmount?: string | null;
- feePerTx?: string;
+ feePerTx?: string; // Solana specific
};
const SelectBarWrapper = styled.div`
@@ -71,7 +71,7 @@ const buildFeeOptions = (
networkType: NetworkType,
symbol: NetworkSymbol,
composedLevels?: PrecomposedLevels | PrecomposedLevelsCardano,
-) => {
+): FeeOptionType[] => {
const filteredLevels = levels.filter(level => level.label !== 'custom');
const getNetworkAmount = (level: FeeLevel) => {
@@ -145,7 +145,7 @@ export const Fees = ({
const errors = props.errors as unknown as FieldErrors;
const error = errors.selectedFee;
- const selectedLevel = feeInfo.levels.find(level => level.label === selectedOption)!;
+ const selectedLevel = feeInfo.levels.find(level => level.label === selectedOption);
const transactionInfo = composedLevels?.[selectedOption];
const feeOptions = buildFeeOptions(feeInfo.levels, networkType, symbol, composedLevels);
@@ -197,7 +197,10 @@ export const Fees = ({
orientation="horizontal"
selectedOption={isCustomFee ? 'custom' : 'normal'}
options={[
- { label: , value: 'normal' },
+ {
+ label: ,
+ value: 'normal',
+ },
{ label: , value: 'custom' },
]}
onChange={() => changeFeeLevel(isCustomFee ? 'normal' : 'custom')}
@@ -207,8 +210,8 @@ export const Fees = ({
)}
- {!isCustomFee && (
- ({
{isCustomFee && (
<>
{
+ const locale = useLocales();
+
+ if (!showFee || !feeOptions.length) {
+ return null;
+ }
+
+ const hasInfo = transactionInfo && transactionInfo.type !== 'error';
+
+ return feeOptions.map((fee, index) => (
+ {fee.label}}
+ topRightChild={
+ <>~{formatDurationStrict(feeInfo.blockTime * (fee?.blocks ?? 0) * 60, locale)}>
+ }
+ bottomLeftChild={
+
+ }
+ bottomRightChild={
+ <>
+
+ {hasInfo ? ` (${transactionInfo.bytes} B)` : ''}
+ >
+ }
+ />
+ ));
+};
diff --git a/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx b/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx
new file mode 100644
index 00000000000..2c04ad0d0fe
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/StandardFee/EthereumFeeCards.tsx
@@ -0,0 +1,41 @@
+import { FeeRate } from '@trezor/product-components';
+
+import { FiatValue } from 'src/components/suite';
+
+import { FeeCard } from './FeeCard';
+import { StandardFeeProps } from './StandardFee';
+
+export const EthereumFeeCards = ({
+ showFee,
+ feeOptions,
+ selectedLevel,
+ changeFeeLevel,
+ symbol,
+ networkType,
+}: StandardFeeProps) => {
+ if (!showFee || !feeOptions.length) {
+ return null;
+ }
+
+ return feeOptions.map((fee, index) => (
+ {fee.label}}
+ topRightChild=""
+ bottomLeftChild={
+
+ }
+ bottomRightChild={
+
+ }
+ />
+ ));
+};
diff --git a/packages/suite/src/components/wallet/Fees/StandardFee/FeeCard.tsx b/packages/suite/src/components/wallet/Fees/StandardFee/FeeCard.tsx
new file mode 100644
index 00000000000..e98da241c6d
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/StandardFee/FeeCard.tsx
@@ -0,0 +1,45 @@
+import { Box, Column, ElevationUp, RadioCard, Row, Text } from '@trezor/components';
+import { FeeLevel } from '@trezor/connect';
+
+type FeeCardProps = {
+ value: FeeLevel['label'];
+ isSelected: boolean;
+ changeFeeLevel: (level: FeeLevel['label']) => void;
+ topLeftChild: React.ReactNode;
+ topRightChild: React.ReactNode;
+ bottomLeftChild: React.ReactNode;
+ bottomRightChild: React.ReactNode;
+};
+
+export const FEE_CARD_MIN_WIDTH = 220;
+
+export const FeeCard = ({
+ value,
+ isSelected,
+ changeFeeLevel,
+ topLeftChild,
+ topRightChild,
+ bottomLeftChild,
+ bottomRightChild,
+}: FeeCardProps) => (
+
+
+ changeFeeLevel(value)} isActive={isSelected}>
+
+
+ {topLeftChild}
+
+ {topRightChild}
+
+
+
+ {bottomLeftChild}
+
+ {bottomRightChild}
+
+
+
+
+
+
+);
diff --git a/packages/suite/src/components/wallet/Fees/StandardFee/MiscFeeCards.tsx b/packages/suite/src/components/wallet/Fees/StandardFee/MiscFeeCards.tsx
new file mode 100644
index 00000000000..dbe6d2e21db
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/StandardFee/MiscFeeCards.tsx
@@ -0,0 +1,51 @@
+import { getFeeUnits } from '@suite-common/wallet-utils';
+import { Text } from '@trezor/components';
+
+import { FiatValue } from 'src/components/suite';
+
+import { FeeCard } from './FeeCard';
+import { StandardFeeProps } from './StandardFee';
+
+// Solana, Ripple, Cardano and other networks with only one option
+export const MiscFeeCards = ({
+ networkType,
+ showFee,
+ feeOptions,
+ symbol,
+ changeFeeLevel,
+}: StandardFeeProps) => {
+ if (!feeOptions?.length || !showFee) return null;
+
+ const isSolanaNetwork = networkType === 'solana';
+
+ const feeOption = feeOptions[0];
+ const shouldShowCurrentFee = !isSolanaNetwork || feeOption.networkAmount;
+ const feeAmount = isSolanaNetwork ? feeOption.feePerTx : feeOption.feePerUnit;
+
+ return (
+ {feeOption.label}
+ }
+ topRightChild=""
+ bottomLeftChild={
+
+ }
+ bottomRightChild={
+ shouldShowCurrentFee && (
+
+ {feeAmount} {getFeeUnits(networkType)}
+
+ )
+ }
+ />
+ );
+};
diff --git a/packages/suite/src/components/wallet/Fees/StandardFee/StandardFee.tsx b/packages/suite/src/components/wallet/Fees/StandardFee/StandardFee.tsx
new file mode 100644
index 00000000000..f7a0f671a3d
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/StandardFee/StandardFee.tsx
@@ -0,0 +1,93 @@
+import { useEffect, useRef, useState } from 'react';
+
+import styled from 'styled-components';
+
+import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
+import {
+ FeeInfo,
+ PrecomposedTransaction,
+ PrecomposedTransactionCardano,
+} from '@suite-common/wallet-types';
+import { Row } from '@trezor/components';
+import { FeeLevel } from '@trezor/connect';
+import { spacings, spacingsPx } from '@trezor/theme';
+
+import { BitcoinFeeCards } from './BitcoinFeeCards';
+import { EthereumFeeCards } from './EthereumFeeCards';
+import { FEE_CARD_MIN_WIDTH } from './FeeCard';
+import { MiscFeeCards } from './MiscFeeCards';
+import { FeeOptionType } from '../Fees';
+
+export type StandardFeeProps = {
+ networkType: NetworkType;
+ symbol: NetworkSymbol;
+ selectedLevel: FeeLevel;
+ // fields below are validated as false-positives, eslint claims that they are not used...
+ feeOptions: FeeOptionType[];
+ feeInfo: FeeInfo;
+ changeFeeLevel: (level: FeeLevel['label']) => void;
+ transactionInfo?: PrecomposedTransaction | PrecomposedTransactionCardano;
+
+ showFee: boolean;
+};
+
+export const FeeCardsWrapper = styled.div<{ $columns: number }>`
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(${({ $columns }) => $columns}, 1fr);
+ gap: ${spacingsPx.sm};
+ align-items: stretch;
+`;
+
+type ResizeObserverType = (
+ feeOptions: FeeOptionType[],
+ setColumns: (columns: number) => void,
+) => ResizeObserver;
+
+export const resizeObserver: ResizeObserverType = (feeOptions, setColumns) =>
+ new ResizeObserver(entries => {
+ const borderBoxSize = entries[0].borderBoxSize?.[0];
+ if (!borderBoxSize) {
+ return;
+ }
+
+ const { inlineSize: elementWidth } = borderBoxSize;
+
+ const minWidth = (FEE_CARD_MIN_WIDTH + spacings.xs) * feeOptions.length;
+
+ const columns = elementWidth > minWidth ? feeOptions.length : 1;
+
+ setColumns(columns);
+ });
+
+export const StandardFee = (props: StandardFeeProps) => {
+ const { networkType, feeOptions } = props;
+
+ const [columns, setColumns] = useState(1);
+
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (!ref.current) return;
+
+ const observer = resizeObserver(feeOptions, setColumns);
+ observer.observe(ref.current);
+
+ return () => observer.disconnect();
+ }, [feeOptions, setColumns]);
+
+ const feeCardsComponentMap: Partial>> = {
+ bitcoin: BitcoinFeeCards,
+ ethereum: EthereumFeeCards,
+ };
+
+ const FeeCardsComponent = feeCardsComponentMap[networkType] ?? MiscFeeCards;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/suite/src/components/wallet/Fees/index.tsx b/packages/suite/src/components/wallet/Fees/index.tsx
new file mode 100644
index 00000000000..6ccf66e50ca
--- /dev/null
+++ b/packages/suite/src/components/wallet/Fees/index.tsx
@@ -0,0 +1,23 @@
+import { CurrentFee } from './CustomFee/CurrentFee';
+import { CustomFee } from './CustomFee/CustomFee';
+import { CustomFeeEthereum } from './CustomFee/CustomFeeEthereum';
+import { CustomFeeMisc } from './CustomFee/CustomFeeMisc';
+import { CustomFeeWrapper } from './CustomFee/CustomFeeWrapper';
+import { BitcoinFeeCards } from './StandardFee/BitcoinFeeCards';
+import { EthereumFeeCards } from './StandardFee/EthereumFeeCards';
+import { FeeCard } from './StandardFee/FeeCard';
+import { MiscFeeCards } from './StandardFee/MiscFeeCards';
+import { StandardFee } from './StandardFee/StandardFee';
+
+export {
+ CustomFee,
+ CurrentFee,
+ CustomFeeEthereum,
+ CustomFeeMisc,
+ CustomFeeWrapper,
+ BitcoinFeeCards,
+ EthereumFeeCards,
+ FeeCard,
+ MiscFeeCards,
+ StandardFee,
+};
diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts
index fbdfe1af120..6116d280951 100644
--- a/packages/suite/src/support/messages.ts
+++ b/packages/suite/src/support/messages.ts
@@ -3418,6 +3418,10 @@ export default defineMessages({
id: 'TR_CURRENT_FEE_CUSTOM_FEES',
defaultMessage: 'Current network fee:',
},
+ GAS_LIMIT_IS_NOT_SET: {
+ id: 'GAS_LIMIT_IS_NOT_SET',
+ defaultMessage: 'Set gas limit for this transaction',
+ },
TR_GAS_LIMIT: {
id: 'TR_GAS_LIMIT',
defaultMessage: 'Gas limit',
diff --git a/suite-common/wallet-config/src/utils.ts b/suite-common/wallet-config/src/utils.ts
index fd3d20270c5..e5f40421892 100644
--- a/suite-common/wallet-config/src/utils.ts
+++ b/suite-common/wallet-config/src/utils.ts
@@ -76,6 +76,14 @@ export const isNetworkSymbol = (symbol: NetworkSymbolExtended): symbol is Networ
*/
export const getNetwork = (symbol: NetworkSymbol): Network => networks[symbol];
+/**
+ * Check wether the network has a settlement layer. Used to check Ethereum L2s.
+ * @param symbol
+ * @returns boolean
+ */
+export const hasNetworkSettlementLayer = (symbol: NetworkSymbol) =>
+ !!getNetwork(symbol).settlementLayer;
+
/**
* Use instead of getNetwork, if there is not a guarantee that the symbol is a valid network symbol.
* @param symbol