From 817360dde20ac400ea4f864d6d47c8b2e48fc1f8 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Mon, 10 Feb 2025 12:02:25 +0100 Subject: [PATCH] feat: implement distinction between BumpFee RBF & Cancel RBF -> utilise it to display error in case TX gets confirmed in cancel flow feat: extend reporting for 'canceled' event in sendform --- packages/suite-analytics/src/types/events.ts | 36 +++++++++-------- .../src/actions/wallet/send/sendFormThunks.ts | 12 +++--- .../suite/src/actions/wallet/stakeActions.ts | 4 +- .../TransactionReviewModalContent.tsx | 40 ++++++++++++++----- .../CancelTransactionModal.tsx | 11 +++-- .../TxDetailModal/ChangeFee/BumpFeeModal.tsx | 2 +- .../ReplaceByFeeFailedOriginalTxConfirmed.tsx | 17 ++++---- .../wallet/replaceByFeeErrorMiddleware.ts | 27 ++++++++----- .../src/utils/suite/transactionReview.ts | 12 ++++-- .../src/send/sendFormBitcoinThunks.ts | 4 +- .../wallet-core/src/send/sendFormThunks.ts | 37 ++++++++--------- .../src/transactions/transactionsThunks.ts | 10 ++--- suite-common/wallet-types/src/transaction.ts | 20 ++++++++-- suite-common/wallet-utils/src/accountUtils.ts | 12 +++--- .../src/reviewTransactionUtils.ts | 10 ++--- .../wallet-utils/src/transactionUtils.ts | 14 ++++++- suite-native/module-send/src/selectors.ts | 6 ++- 17 files changed, 169 insertions(+), 105 deletions(-) diff --git a/packages/suite-analytics/src/types/events.ts b/packages/suite-analytics/src/types/events.ts index 623ed1778fb..d9f2e3c9ae5 100644 --- a/packages/suite-analytics/src/types/events.ts +++ b/packages/suite-analytics/src/types/events.ts @@ -36,6 +36,24 @@ export type SuiteAnalyticsEventSuiteReady = { }; }; +export type TransactionCreatedEvent = { + type: EventType.TransactionCreated; + payload: { + action: 'sent' | 'copied' | 'downloaded' | 'replaced' | 'canceled'; + symbol: string; + tokens: string; + outputsCount: number; + broadcast: boolean; + bitcoinLockTime: boolean; + ethereumData: boolean; + ethereumNonce: boolean; + rippleDestinationTag: boolean; + selectedFee: string; + isCoinControlEnabled: boolean; + hasCoinControlBeenOpened: boolean; + }; +}; + export type SuiteAnalyticsEvent = | SuiteAnalyticsEventSuiteReady | { @@ -198,23 +216,7 @@ export type SuiteAnalyticsEvent = type: 'exchange' | 'buy' | 'sell'; }; } - | { - type: EventType.TransactionCreated; - payload: { - action: 'sent' | 'copied' | 'downloaded' | 'replaced'; - symbol: string; - tokens: string; - outputsCount: number; - broadcast: boolean; - bitcoinLockTime: boolean; - ethereumData: boolean; - ethereumNonce: boolean; - rippleDestinationTag: boolean; - selectedFee: string; - isCoinControlEnabled: boolean; - hasCoinControlBeenOpened: boolean; - }; - } + | TransactionCreatedEvent | { type: EventType.SendRawTransaction; payload: { diff --git a/packages/suite/src/actions/wallet/send/sendFormThunks.ts b/packages/suite/src/actions/wallet/send/sendFormThunks.ts index ac9a95e553c..37dd4476612 100644 --- a/packages/suite/src/actions/wallet/send/sendFormThunks.ts +++ b/packages/suite/src/actions/wallet/send/sendFormThunks.ts @@ -18,9 +18,9 @@ import { Account, FormState, GeneralPrecomposedTransactionFinal, - PrecomposedTransactionFinalRbf, + PrecomposedTransactionFinalBumpFeeRbf, } from '@suite-common/wallet-types'; -import { isCardanoTx, isRbfTransaction } from '@suite-common/wallet-utils'; +import { isCardanoTx, isRbfBumpFeeTransaction } from '@suite-common/wallet-utils'; import { getSynchronize } from '@trezor/utils'; import * as metadataLabelingActions from 'src/actions/suite/metadataLabelingActions'; @@ -100,7 +100,7 @@ const updateRbfLabelsThunk = createThunk( txid, }: { labelsToBeEdited: RbfLabelsToBeUpdated; - precomposedTransaction: PrecomposedTransactionFinalRbf; + precomposedTransaction: PrecomposedTransactionFinalBumpFeeRbf; txid: string; }, { dispatch }, @@ -248,10 +248,10 @@ export const signAndPushSendFormTransactionThunk = createThunk( return; } - const isRbf = isRbfTransaction(precomposedTransaction); + const isBumpFeeRbf = isRbfBumpFeeTransaction(precomposedTransaction); // This has to be executed prior to pushing the transaction! - const rbfLabelsToBeEdited = isRbf + const rbfLabelsToBeEdited = isBumpFeeRbf ? dispatch(findLabelsToBeMovedOrDeleted({ prevTxid: precomposedTransaction.prevTxid })) : null; @@ -269,7 +269,7 @@ export const signAndPushSendFormTransactionThunk = createThunk( const result = pushResponse.payload; const { txid } = result.payload; - if (isRbf && rbfLabelsToBeEdited) { + if (isBumpFeeRbf && rbfLabelsToBeEdited) { dispatch( updateRbfLabelsThunk({ labelsToBeEdited: rbfLabelsToBeEdited, diff --git a/packages/suite/src/actions/wallet/stakeActions.ts b/packages/suite/src/actions/wallet/stakeActions.ts index 5dbd6878191..5904cf15b5e 100644 --- a/packages/suite/src/actions/wallet/stakeActions.ts +++ b/packages/suite/src/actions/wallet/stakeActions.ts @@ -9,7 +9,7 @@ import { import { PrecomposedTransactionFinal, StakeFormState, StakeType } from '@suite-common/wallet-types'; import { formatNetworkAmount, - isRbfTransaction, + isRbfBumpFeeTransaction, isSupportedEthStakingNetworkSymbol, isSupportedSolStakingNetworkSymbol, tryGetAccountIdentity, @@ -118,7 +118,7 @@ const pushTransaction = ); } - if (isRbfTransaction(precomposedTx)) { + if (isRbfBumpFeeTransaction(precomposedTx)) { // notification from the backend may be delayed. // modify affected transaction(s) in the reducer until the real account update occurs. // this will update transaction details (like time, fee etc.) diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx index cc83a639d36..9a81c0b0656 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx @@ -10,16 +10,18 @@ import { selectSendFormReviewButtonRequestsCount, selectStakePrecomposedForm, } from '@suite-common/wallet-core'; -import { FormState, StakeFormState } from '@suite-common/wallet-types'; +import { FormState, RbfTransactionType, StakeFormState } from '@suite-common/wallet-types'; import { constructTransactionReviewOutputs, getTxStakeNameByDataHex, + isRbfBumpFeeTransaction, + isRbfCancelTransaction, isRbfTransaction, } from '@suite-common/wallet-utils'; import { NewModal } from '@trezor/components'; import { copyToClipboard, download } from '@trezor/dom-utils'; import { ConfirmOnDevice } from '@trezor/product-components'; -import { EventType, analytics } from '@trezor/suite-analytics'; +import { EventType, TransactionCreatedEvent, analytics } from '@trezor/suite-analytics'; import { Deferred } from '@trezor/utils'; import * as modalActions from 'src/actions/suite/modalActions'; @@ -40,6 +42,14 @@ const isStakeState = (state: SendState | StakeState): state is StakeState => 'da const isStakeForm = (form: FormState | StakeFormState): form is StakeFormState => 'stakeType' in form; +const mapRbfTypeToReporting: Record< + RbfTransactionType, + TransactionCreatedEvent['payload']['action'] +> = { + 'bump-fee': 'replaced', + cancel: 'canceled', +}; + type TransactionReviewModalContentProps = { decision: Deferred | undefined; txInfoState: SendState | StakeState; @@ -70,10 +80,13 @@ export const TransactionReviewModalContent = ({ ); const isTradingAction = !!precomposedForm?.isTrading; - const isRbfAction = precomposedTx !== undefined && isRbfTransaction(precomposedTx); + const isBumpFeeRbfAction = + precomposedTx !== undefined && isRbfBumpFeeTransaction(precomposedTx); const decreaseOutputId = - isRbfAction && precomposedTx.useNativeRbf ? precomposedForm?.setMaxOutputId : undefined; + isBumpFeeRbfAction && precomposedTx.useNativeRbf + ? precomposedForm?.setMaxOutputId + : undefined; const buttonRequestsCount = useSelector((state: DeviceRootState) => selectSendFormReviewButtonRequestsCount(state, account?.symbol, decreaseOutputId), @@ -112,15 +125,18 @@ export const TransactionReviewModalContent = ({ } }; + const isCancelRbfAction = isRbfCancelTransaction(precomposedTx); + const actionLabel = getTransactionReviewModalActionText({ stakeType, - isRbfAction, + isBumpFeeRbfAction, + isCancelRbfAction, isSending, }); const isBroadcastEnabled = options.includes('broadcast'); - const reportTransactionCreatedEvent = (action: 'sent' | 'copied' | 'downloaded' | 'replaced') => + const reportTransactionCreatedEvent = (action: TransactionCreatedEvent['payload']['action']) => analytics.report({ type: EventType.TransactionCreated, payload: { @@ -148,7 +164,11 @@ export const TransactionReviewModalContent = ({ } if (decision) { decision.resolve(true); - reportTransactionCreatedEvent(isRbfAction ? 'replaced' : 'sent'); + reportTransactionCreatedEvent( + isRbfTransaction(precomposedTx) + ? mapRbfTypeToReporting[precomposedTx.rbfType] + : 'sent', + ); } }; @@ -222,8 +242,8 @@ export const TransactionReviewModalContent = ({ return ; } - if (isRbfConfirmedError) { - return ; + if (isRbfConfirmedError && isRbfTransaction(precomposedTx)) { + return ; } return ( @@ -233,7 +253,7 @@ export const TransactionReviewModalContent = ({ signedTx={serializedTx} outputs={outputs} buttonRequestsCount={buttonRequestsCount} - isRbfAction={isRbfAction} + isRbfAction={isBumpFeeRbfAction} isTradingAction={isTradingAction} isSending={isSending} stakeType={stakeType || undefined} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransactionModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransactionModal.tsx index 3a69e36495d..60170024cfb 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransactionModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/CancelTransaction/CancelTransactionModal.tsx @@ -8,11 +8,11 @@ import { import { Account, ChainedTransactions, + PrecomposedTransactionFinalCancelRbf, SelectedAccountLoaded, WalletAccountTransactionWithRequiredRbfParams, } from '@suite-common/wallet-types'; import { Banner, Column, NewModal } from '@trezor/components'; -import { PrecomposeResultFinal } from '@trezor/connect'; import { spacings } from '@trezor/theme'; import { CancelTransaction } from './CancelTransaction'; @@ -50,7 +50,8 @@ export const CancelTransactionModal = ({ const { account } = selectedAccount; const dispatch = useDispatch(); - const [composedCancelTx, setComposedCancelTx] = useState(null); + const [composedCancelTx, setComposedCancelTx] = + useState(null); const confirmations = useSelector(state => selectTransactionConfirmations(state, tx.txid, account.key), @@ -69,7 +70,9 @@ export const CancelTransactionModal = ({ dispatch(composeCancelTransactionThunk({ account, tx, chainedTxs })) .unwrap() - .then(setComposedCancelTx) + .then(precomposed => { + setComposedCancelTx({ ...precomposed, rbfType: 'cancel', prevTxid: tx.txid }); + }) .catch(setError); }, [account, tx, dispatch, chainedTxs]); @@ -100,7 +103,7 @@ export const CancelTransactionModal = ({ onBackClick={onBackClick} > {isTxConfirmed ? ( - + ) : ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/BumpFeeModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/BumpFeeModal.tsx index d569529b209..e837420624d 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/BumpFeeModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/BumpFeeModal.tsx @@ -58,7 +58,7 @@ export const BumpFeeModal = ({ onBackClick={onBackClick} > {isTxConfirmed ? ( - + ) : ( )} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ReplaceByFeeFailedOriginalTxConfirmed.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ReplaceByFeeFailedOriginalTxConfirmed.tsx index e6759d6fd64..ca326df212f 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ReplaceByFeeFailedOriginalTxConfirmed.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ReplaceByFeeFailedOriginalTxConfirmed.tsx @@ -1,3 +1,4 @@ +import { RbfTransactionType } from '@suite-common/wallet-types'; import { Box, Card, Column, IconCircle, Text } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { @@ -9,23 +10,23 @@ import { import { Translation, TranslationKey } from '../../../../Translation'; import { TrezorLink } from '../../../../TrezorLink'; -type ReplaceByFeeFailedOriginalTxConfirmedProps = { - type: 'replace-by-fee' | 'cancel-transaction'; +export type ReplaceByFeeFailedOriginalTxConfirmedProps = { + type: RbfTransactionType; }; const titleMap: Record = { - 'replace-by-fee': 'TR_REPLACE_BY_FEE_FAILED_ALREADY_MINED', - 'cancel-transaction': 'TR_CANCEL_TX_FAILED_ALREADY_MINED', + 'bump-fee': 'TR_REPLACE_BY_FEE_FAILED_ALREADY_MINED', + cancel: 'TR_CANCEL_TX_FAILED_ALREADY_MINED', }; const descriptionMap: Record = { - 'replace-by-fee': 'TR_REPLACE_BY_FEE_FAILED_ALREADY_MINED_DESCRIPTION', - 'cancel-transaction': 'TR_CANCEL_TX_FAILED_ALREADY_MINED_DESCRIPTION', + 'bump-fee': 'TR_REPLACE_BY_FEE_FAILED_ALREADY_MINED_DESCRIPTION', + cancel: 'TR_CANCEL_TX_FAILED_ALREADY_MINED_DESCRIPTION', }; const helpLink: Record = { - 'replace-by-fee': HELP_CENTER_REPLACE_BY_FEE_BITCOIN, - 'cancel-transaction': HELP_CENTER_CANCEL_TRANSACTION, + 'bump-fee': HELP_CENTER_REPLACE_BY_FEE_BITCOIN, + cancel: HELP_CENTER_CANCEL_TRANSACTION, }; export const ReplaceByFeeFailedOriginalTxConfirmed = ({ diff --git a/packages/suite/src/middlewares/wallet/replaceByFeeErrorMiddleware.ts b/packages/suite/src/middlewares/wallet/replaceByFeeErrorMiddleware.ts index 487d4ada18c..fd581b2925c 100644 --- a/packages/suite/src/middlewares/wallet/replaceByFeeErrorMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/replaceByFeeErrorMiddleware.ts @@ -13,20 +13,25 @@ export const replaceByFeeErrorMiddleware = (action: Action): Action => { next(action); - if (transactionsActions.addTransaction.match(action)) { - const { transactions } = action.payload; + if (!transactionsActions.addTransaction.match(action)) { + return action; + } + + const { transactions } = action.payload; + const precomposedTx = api.getState().wallet.send?.precomposedTx; - const precomposedTx = api.getState().wallet.send?.precomposedTx; + if (precomposedTx === undefined) { + return action; + } + + if (!isRbfTransaction(precomposedTx)) { + return action; + } - if (precomposedTx !== undefined && isRbfTransaction(precomposedTx)) { - const addedTransaction = transactions.find( - tx => tx.txid === precomposedTx.prevTxid, - ); + const addedTransaction = transactions.find(tx => tx.txid === precomposedTx.prevTxid); - if (addedTransaction !== undefined && addedTransaction.blockHeight !== undefined) { - api.dispatch(replaceByFeeErrorThunk()); - } - } + if (addedTransaction !== undefined && addedTransaction.blockHeight !== undefined) { + api.dispatch(replaceByFeeErrorThunk()); } return action; diff --git a/packages/suite/src/utils/suite/transactionReview.ts b/packages/suite/src/utils/suite/transactionReview.ts index 8aa29ac9e56..1171d33275e 100644 --- a/packages/suite/src/utils/suite/transactionReview.ts +++ b/packages/suite/src/utils/suite/transactionReview.ts @@ -3,13 +3,15 @@ import { StakeFormState } from '@suite-common/wallet-types'; interface getTransactionReviewModalActionTextParams { stakeType: StakeFormState['stakeType'] | null; - isRbfAction: boolean; + isBumpFeeRbfAction: boolean; + isCancelRbfAction: boolean; isSending?: boolean; } export const getTransactionReviewModalActionText = ({ stakeType, - isRbfAction, + isBumpFeeRbfAction, + isCancelRbfAction, isSending, }: getTransactionReviewModalActionTextParams): TranslationKey => { switch (stakeType) { @@ -22,10 +24,14 @@ export const getTransactionReviewModalActionText = ({ // no default } - if (isRbfAction) { + if (isBumpFeeRbfAction) { return 'TR_REPLACE_TX'; } + if (isCancelRbfAction) { + return 'TR_CANCEL_TX'; + } + if (isSending) { return 'TR_CONFIRMING_TX'; } diff --git a/suite-common/wallet-core/src/send/sendFormBitcoinThunks.ts b/suite-common/wallet-core/src/send/sendFormBitcoinThunks.ts index e1503f02b77..3b8f73f04f8 100644 --- a/suite-common/wallet-core/src/send/sendFormBitcoinThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormBitcoinThunks.ts @@ -14,7 +14,7 @@ import { getBitcoinComposeOutputs, getUtxoOutpoint, hasNetworkFeatures, - isRbfTransaction, + isRbfBumpFeeTransaction, restoreOrigOutputsOrder, } from '@suite-common/wallet-utils'; import TrezorConnect, { @@ -273,7 +273,7 @@ export const signBitcoinSendFormTransactionThunk = createThunk< if ( formState.rbfParams && - isRbfTransaction(precomposedTransaction) && + isRbfBumpFeeTransaction(precomposedTransaction) && precomposedTransaction.useNativeRbf ) { const { txid, utxo, outputs } = formState.rbfParams; diff --git a/suite-common/wallet-core/src/send/sendFormThunks.ts b/suite-common/wallet-core/src/send/sendFormThunks.ts index 46d7584fd15..43a7e3c127e 100644 --- a/suite-common/wallet-core/src/send/sendFormThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormThunks.ts @@ -13,8 +13,8 @@ import { PrecomposedLevels, PrecomposedLevelsCardano, PrecomposedTransactionFinal, + PrecomposedTransactionFinalBumpFeeRbf, PrecomposedTransactionFinalCardano, - PrecomposedTransactionFinalRbf, } from '@suite-common/wallet-types'; import { amountToSmallestUnit, @@ -533,25 +533,26 @@ export const enhancePrecomposedTransactionThunk = createThunk< (!hasDecreasedOutput && nativeRbfAvailable) || (hasDecreasedOutput && decreaseOutputAvailable); - const enhancedPrecomposedTransaction: GeneralPrecomposedTransactionFinal = { - ...precomposedTransaction, - }; - - if (!isCardanoTx(selectedAccount, enhancedPrecomposedTransaction)) { - if (formValues.rbfParams) { - (enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).prevTxid = - formValues.rbfParams.txid; - (enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).feeDifference = - new BigNumber(precomposedTransaction.fee) + const createGeneralPrecomposedTransactionFinal = (): GeneralPrecomposedTransactionFinal => { + if (!isCardanoTx(selectedAccount, precomposedTransaction) && formValues.rbfParams) { + const enhancedRbfPrecomposedTx: PrecomposedTransactionFinalBumpFeeRbf = { + ...precomposedTransaction, + rbfType: 'bump-fee', + prevTxid: formValues.rbfParams.txid, + feeDifference: new BigNumber(precomposedTransaction.fee) .minus(formValues.rbfParams.baseFee) - .toFixed(); - (enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).useNativeRbf = - !!useNativeRbf; - ( - enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf - ).useDecreaseOutput = !!hasDecreasedOutput; + .toFixed(), + useNativeRbf: !!useNativeRbf, + useDecreaseOutput: !!hasDecreasedOutput, + }; + + return enhancedRbfPrecomposedTx; } - } + + return precomposedTransaction; + }; + + const enhancedPrecomposedTransaction = createGeneralPrecomposedTransactionFinal(); if ( !isCardanoTx(selectedAccount, enhancedPrecomposedTransaction) && diff --git a/suite-common/wallet-core/src/transactions/transactionsThunks.ts b/suite-common/wallet-core/src/transactions/transactionsThunks.ts index 4d64c09c941..87781c4e82e 100644 --- a/suite-common/wallet-core/src/transactions/transactionsThunks.ts +++ b/suite-common/wallet-core/src/transactions/transactionsThunks.ts @@ -5,7 +5,7 @@ import { AccountKey, PrecomposedTransactionCardanoFinal, PrecomposedTransactionFinal, - PrecomposedTransactionFinalRbf, + PrecomposedTransactionFinalBumpFeeRbf, Timestamp, WalletAccountTransaction, } from '@suite-common/wallet-types'; @@ -15,7 +15,7 @@ import { findTransactions, getPendingAccount, getRbfParams, - isRbfTransaction, + isRbfBumpFeeTransaction, isTrezorConnectBackendType, replaceEthereumSpecific, tryGetAccountIdentity, @@ -44,7 +44,7 @@ import { selectSendSignedTx } from '../send/sendFormReducer'; */ interface ReplaceTransactionThunkParams { // transaction input parameters. It has to be passed as argument rather than obtained form send-form state, because this thunk is used also by eth-staking module that uses different redux state. - precomposedTransaction: PrecomposedTransactionFinalRbf; + precomposedTransaction: PrecomposedTransactionFinalBumpFeeRbf; newTxid: string; } @@ -54,7 +54,7 @@ export const replaceTransactionThunk = createThunk( { precomposedTransaction, newTxid }: ReplaceTransactionThunkParams, { getState, dispatch }, ) => { - if (!isRbfTransaction(precomposedTransaction)) return; // ignore if it's not a replacement tx + if (!isRbfBumpFeeTransaction(precomposedTransaction)) return; // ignore if it's not a replacement tx const walletTransactions = selectTransactions(getState()); const signedTransaction = selectSendSignedTx(getState()); @@ -156,7 +156,7 @@ export const addFakePendingTxThunk = createThunk( Object.keys(affectedAccounts).forEach(key => { const affectedAccount = affectedAccounts[key]; - if (!isRbfTransaction(precomposedTransaction)) { + if (!isRbfBumpFeeTransaction(precomposedTransaction)) { // create and profile pending transaction for affected account if it's not a replacement tx const affectedAccountTransaction = blockbookUtils.transformTransaction( signedTransaction, diff --git a/suite-common/wallet-types/src/transaction.ts b/suite-common/wallet-types/src/transaction.ts index e1ae2bd78ca..0898cf17365 100644 --- a/suite-common/wallet-types/src/transaction.ts +++ b/suite-common/wallet-types/src/transaction.ts @@ -103,7 +103,7 @@ export type PrecomposedTransactionNonFinal = PrecomposedTransactionConnectRespon }; // base of PrecomposedTransactionFinal -type TxFinal = PrecomposedTransactionConnectResponseFinal & { +type PrecomposedTransactionBase = PrecomposedTransactionConnectResponseFinal & { max?: string; feeLimit?: string; estimatedFeeLimit?: string; @@ -120,7 +120,10 @@ export type PrecomposedTransactionCardanoFinal = token?: TokenInfo; }; -export type PrecomposedTransactionFinalRbf = TxFinal & { +export type RbfTransactionType = 'bump-fee' | 'cancel'; + +export type PrecomposedTransactionFinalBumpFeeRbf = PrecomposedTransactionBase & { + rbfType: 'bump-fee'; prevTxid: string; feeDifference: string; // Native RBF is a firmware feature to recognize an RBF transaction and simplify transaction review flow. @@ -128,8 +131,16 @@ export type PrecomposedTransactionFinalRbf = TxFinal & { useDecreaseOutput: boolean; }; -// strict distinction between normal and RBF type -export type PrecomposedTransactionFinal = TxFinal | PrecomposedTransactionFinalRbf; +export type PrecomposedTransactionFinalCancelRbf = PrecomposedTransactionBase & { + rbfType: 'cancel'; + prevTxid: string; +}; + +// Strict distinction between Normal-Tx and Bump-Fee-Tx / Cancel-Tx +export type PrecomposedTransactionFinal = + | PrecomposedTransactionBase + | PrecomposedTransactionFinalBumpFeeRbf + | PrecomposedTransactionFinalCancelRbf; export type PrecomposedTransaction = | PrecomposedTransactionError @@ -142,6 +153,7 @@ export type PrecomposedTransactionCardano = | PrecomposedTransactionCardanoFinal; export type GeneralPrecomposedTransaction = PrecomposedTransaction | PrecomposedTransactionCardano; + export type GeneralPrecomposedTransactionFinal = Extract< GeneralPrecomposedTransaction, { type: 'final' } diff --git a/suite-common/wallet-utils/src/accountUtils.ts b/suite-common/wallet-utils/src/accountUtils.ts index 246e25f5b8a..5036c5f291f 100644 --- a/suite-common/wallet-utils/src/accountUtils.ts +++ b/suite-common/wallet-utils/src/accountUtils.ts @@ -46,7 +46,7 @@ import { BigNumber, BigNumberValue } from '@trezor/utils/src/bigNumber'; import { toFiatCurrency } from './fiatConverterUtils'; import { getFiatRateKey } from './fiatRatesUtils'; import { getAccountTotalStakingBalance } from './stakingUtils'; -import { isRbfTransaction } from './transactionUtils'; +import { isRbfBumpFeeTransaction } from './transactionUtils'; export const isUtxoBased = (account: Account) => account.networkType === 'bitcoin' || account.networkType === 'cardano'; @@ -1014,21 +1014,23 @@ export const getPendingAccount = ({ // calculate availableBalance let availableBalanceBig = new BigNumber(account.availableBalance); - const isRbf = isRbfTransaction(tx); + const isBumpFeeRbf = isRbfBumpFeeTransaction(tx); if (!receivingAccount) { - availableBalanceBig = availableBalanceBig.minus(isRbf ? tx.feeDifference : tx.totalSpent); + availableBalanceBig = availableBalanceBig.minus( + isBumpFeeRbf ? tx.feeDifference : tx.totalSpent, + ); } // get utxo const utxo = getUtxoFromSignedTransaction({ account, tx, txid, - prevTxid: isRbf ? tx.prevTxid : undefined, + prevTxid: isBumpFeeRbf ? tx.prevTxid : undefined, receivingAccount, }); - if (!isRbf) { + if (!isBumpFeeRbf) { // join all account addresses const addresses = getAccountAddresses(account); diff --git a/suite-common/wallet-utils/src/reviewTransactionUtils.ts b/suite-common/wallet-utils/src/reviewTransactionUtils.ts index d07aea40759..74ef002c654 100644 --- a/suite-common/wallet-utils/src/reviewTransactionUtils.ts +++ b/suite-common/wallet-utils/src/reviewTransactionUtils.ts @@ -14,7 +14,7 @@ import { getFirmwareVersion } from '@trezor/device-utils'; import { versionUtils } from '@trezor/utils'; import { getShortFingerprint, isCardanoTx } from './cardanoUtils'; -import { isRbfTransaction } from './transactionUtils'; +import { isRbfBumpFeeTransaction } from './transactionUtils'; export const getTransactionReviewOutputState = ( index: number, @@ -102,10 +102,10 @@ const constructOldFlow = ({ const hasBitcoinLockTime = 'bitcoinLockTime' in precomposedForm; const hasRippleDestinationTag = 'rippleDestinationTag' in precomposedForm; - const isRbf = isRbfTransaction(precomposedTx); + const isBumpFeeRbf = isRbfBumpFeeTransaction(precomposedTx); // used in the bump fee flow - if (isRbf && precomposedTx.useNativeRbf) { + if (isBumpFeeRbf && precomposedTx.useNativeRbf) { outputs.push( { type: 'txid', @@ -192,7 +192,7 @@ const constructOldFlow = ({ value: precomposedForm.rippleDestinationTag, }); } - } else if (!isRbf || !precomposedTx.useNativeRbf) { + } else if (!isBumpFeeRbf || !precomposedTx.useNativeRbf) { outputs.push({ type: 'fee', value: precomposedTx.fee }); } @@ -219,7 +219,7 @@ const constructNewFlow = ({ outputs.push({ type: 'data', value: precomposedForm.ethereumDataHex }); } - const isRbf = isRbfTransaction(precomposedTx); + const isRbf = isRbfBumpFeeTransaction(precomposedTx); // used in the bump fee flow if (isRbf && precomposedTx.useNativeRbf) { diff --git a/suite-common/wallet-utils/src/transactionUtils.ts b/suite-common/wallet-utils/src/transactionUtils.ts index 24b544f5f55..4c2b916eab7 100644 --- a/suite-common/wallet-utils/src/transactionUtils.ts +++ b/suite-common/wallet-utils/src/transactionUtils.ts @@ -10,7 +10,8 @@ import { ChainedTransactions, GeneralPrecomposedTransactionFinal, PrecomposedTransactionFinal, - PrecomposedTransactionFinalRbf, + PrecomposedTransactionFinalBumpFeeRbf, + PrecomposedTransactionFinalCancelRbf, RatesByTimestamps, RbfTransactionParams, Timestamp, @@ -64,7 +65,16 @@ export const isPending = (tx: WalletAccountTransaction | AccountTransaction) => export const isRbfTransaction = ( tx: GeneralPrecomposedTransactionFinal, -): tx is PrecomposedTransactionFinalRbf => 'prevTxid' in tx && tx.prevTxid !== undefined; +): tx is PrecomposedTransactionFinalBumpFeeRbf | PrecomposedTransactionFinalCancelRbf => + 'rbfType' in tx; + +export const isRbfBumpFeeTransaction = ( + tx: GeneralPrecomposedTransactionFinal, +): tx is PrecomposedTransactionFinalBumpFeeRbf => isRbfTransaction(tx) && tx.rbfType === 'bump-fee'; + +export const isRbfCancelTransaction = ( + tx: GeneralPrecomposedTransactionFinal, +): tx is PrecomposedTransactionFinalCancelRbf => isRbfTransaction(tx) && tx.rbfType === 'cancel'; /* Convert date to string in YYYY-MM-DD format */ const generateTransactionDateKey = (d: Date) => diff --git a/suite-native/module-send/src/selectors.ts b/suite-native/module-send/src/selectors.ts index 3212938f0ac..9fd5adba5d7 100644 --- a/suite-native/module-send/src/selectors.ts +++ b/suite-native/module-send/src/selectors.ts @@ -16,7 +16,7 @@ import { constructTransactionReviewOutputs, getIsUpdatedSendFlow, getTransactionReviewOutputState, - isRbfTransaction, + isRbfBumpFeeTransaction, } from '@suite-common/wallet-utils'; import { StatefulReviewOutput } from './types'; @@ -30,7 +30,9 @@ export const selectTransactionReviewOutputs = ( const precomposedTx = selectSendPrecomposedTx(state); const decreaseOutputId = - precomposedTx !== undefined && isRbfTransaction(precomposedTx) && precomposedTx.useNativeRbf + precomposedTx !== undefined && + isRbfBumpFeeTransaction(precomposedTx) && + precomposedTx.useNativeRbf ? precomposedForm?.setMaxOutputId : undefined;