diff --git a/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx b/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx index 629e7e1e094..19649a8bcf5 100644 --- a/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx +++ b/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx @@ -2,7 +2,7 @@ import { memo, useMemo, useState } from 'react'; import styled, { css } from 'styled-components'; import { AnimatePresence } from 'framer-motion'; import { selectIsPhishingTransaction } from '@suite-common/wallet-core'; -import { variables, Button, Card } from '@trezor/components'; +import { variables, Button, Card, Link } from '@trezor/components'; import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; @@ -35,6 +35,8 @@ import { BlurWrapper } from './TransactionItemBlurWrapper'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; import { getInstantStakeType } from 'src/utils/suite/stake'; import { isStakeTypeTx } from '@suite-common/suite-utils'; +import { Tooltip } from '@trezor/components'; +import { HELP_CENTER_REPLACE_BY_FEE } from '@trezor/urls'; // eslint-disable-next-line local-rules/no-override-ds-component const Wrapper = styled(Card)<{ @@ -88,6 +90,7 @@ interface TransactionItemProps { network: Network; accountType: AccountType; className?: string; + disableBumpFee?: boolean; index: number; } @@ -101,6 +104,7 @@ export const TransactionItem = memo( network, accountType, className, + disableBumpFee, index, }: TransactionItemProps) => { const [limit, setLimit] = useState(0); @@ -183,6 +187,42 @@ export const TransactionItem = memo( transaction.deadline ? '/prepending' : '' }`; + const BumpFeeButton = ({ isDisabled }: { isDisabled: boolean }) => ( + + ); + + const DisabledBumpFeeButtonWithTooltip = () => ( + + ( + + {chunks} + + ), + }} + /> + + } + > + + + ); + // we are using slightly different layout for 1 targets txs to better match the design // the only difference is that crypto amount is in the same row as tx heading/description // fiat amount is in the second row along with address @@ -418,12 +458,11 @@ export const TransactionItem = memo( networkFeatures?.includes('rbf') && !transaction?.deadline && ( - + {disableBumpFee ? ( + + ) : ( + + )} )} diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index f6494b9a3ee..0b8439a7819 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -9157,4 +9157,9 @@ export default defineMessages({ defaultMessage: 'Setting a low fee might cause your transaction to fail or experience significant delays.', }, + TR_BUMP_FEE_DISABLED_TOOLTIP: { + id: 'TR_BUMP_FEE_DISABLED_TOOLTIP', + defaultMessage: + 'To speed up your transactions, increase the fee on the oldest (by nonce) pending transaction in the queue. Transactions must be confirmed in order. Learn more', + }, }); diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx index 1eb4d21ed68..92add38607c 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionGroupedList.tsx @@ -1,4 +1,8 @@ -import { GroupedTransactionsByDate, groupJointTransactions } from '@suite-common/wallet-utils'; +import { + getTransactionWithLowestNonce, + GroupedTransactionsByDate, + groupJointTransactions, +} from '@suite-common/wallet-utils'; import { getNetwork } from '@suite-common/wallet-config'; import { CoinjoinBatchItem } from 'src/components/wallet/TransactionItem/CoinjoinBatchItem'; import { useSelector } from 'src/hooks/suite'; @@ -25,6 +29,9 @@ export const TransactionGroupedList = ({ const accountMetadata = useSelector(state => selectLabelingDataForAccount(state, account.key)); const network = getNetwork(symbol); + const transactionWithLowestNonce: WalletAccountTransaction | null = + getTransactionWithLowestNonce(transactionGroups); + return Object.entries(transactionGroups).map(([dateKey, value], groupIndex) => ( ), )} diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts index ef910109009..19161878d7f 100644 --- a/packages/urls/src/urls.ts +++ b/packages/urls/src/urls.ts @@ -103,6 +103,8 @@ export const HELP_CENTER_EVM_ADDRESS_CHECKSUM: Url = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite'; export const HELP_CENTER_FIRMWARE_REVISION_CHECK: Url = 'https://trezor.io/learn/a/trezor-firmware-revision-check'; +export const HELP_CENTER_REPLACE_BY_FEE: Url = + 'https://trezor.io/learn/a/replace-by-fee-rbf-ethereum'; export const INVITY_URL: Url = 'https://invity.io/'; export const INVITY_SCHEDULE_OF_FEES: Url = 'https://blog.invity.io/schedule-of-fees'; diff --git a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts index 9aa1a01dc57..f512caa950d 100644 --- a/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts +++ b/suite-common/wallet-utils/src/__tests__/transactionUtils.test.ts @@ -18,6 +18,7 @@ import { MonthKey, generateTransactionMonthKey, groupTokensTransactionsByContractAddress, + getTransactionWithLowestNonce, } from '../transactionUtils'; const { getWalletTransaction } = testMocks; @@ -101,6 +102,41 @@ describe('transaction utils', () => { }); }); + it('getTransactionWithLowestNonce - ethereum network', () => { + const transactionGroups = { + '2019-10-3': [getWalletTransaction({ ethereumSpecific: { nonce: 1 } as any })], + '2019-10-4': [ + getWalletTransaction({ + ethereumSpecific: { nonce: 0 }, + } as any), + ], + '2019-8-14': [ + getWalletTransaction({ ethereumSpecific: { nonce: 2 } as any }), + getWalletTransaction({ ethereumSpecific: { nonce: 3 } as any }), + ], + }; + + const transactionWithLowestNonce: WalletAccountTransaction | null = + getTransactionWithLowestNonce(transactionGroups); + + expect(transactionWithLowestNonce).toStrictEqual( + getWalletTransaction({ ethereumSpecific: { nonce: 0 } as any }), + ); + }); + + it('getTransactionWithLowestNonce - non ethereum network', () => { + const transactionGroups = { + '2019-10-4': [getWalletTransaction()], + '2019-10-3': [getWalletTransaction()], + '2019-8-14': [getWalletTransaction(), getWalletTransaction()], + }; + + const transactionWithLowestNonce: WalletAccountTransaction | null = + getTransactionWithLowestNonce(transactionGroups); + + expect(transactionWithLowestNonce).toStrictEqual(null); + }); + it('groupJointTransactions', () => { const [j1, r2, j3, j4, s5, s6, j7, f8, j9, j10, j11] = ( [ diff --git a/suite-common/wallet-utils/src/transactionUtils.ts b/suite-common/wallet-utils/src/transactionUtils.ts index 182db04bbe3..12b0be8287e 100644 --- a/suite-common/wallet-utils/src/transactionUtils.ts +++ b/suite-common/wallet-utils/src/transactionUtils.ts @@ -1034,3 +1034,21 @@ export const groupTokensTransactionsByContractAddress = ( return groupedTokensTxs; }; + +export const getTransactionWithLowestNonce = ( + transactionGroups: GroupedTransactionsByDate, +): WalletAccountTransaction | null => + Object.values(transactionGroups) + .flat() + .reduce((lowestNonceTransaction, transaction) => { + if (!transaction.ethereumSpecific) return null; + const currentNonce = Number(transaction.ethereumSpecific?.nonce); + if ( + lowestNonceTransaction === null || + currentNonce < Number(lowestNonceTransaction.ethereumSpecific?.nonce) + ) { + lowestNonceTransaction = transaction; + } + + return lowestNonceTransaction; + }, null);