Skip to content

Commit

Permalink
feat: implement distinction between BumpFee RBF & Cancel RBF -> utili…
Browse files Browse the repository at this point in the history
…se it to display error in case TX gets confirmed in cancel flow
  • Loading branch information
peter-sanderson committed Feb 10, 2025
1 parent 763db76 commit f1a775a
Show file tree
Hide file tree
Showing 14 changed files with 114 additions and 58 deletions.
12 changes: 6 additions & 6 deletions packages/suite/src/actions/wallet/send/sendFormThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -100,7 +100,7 @@ const updateRbfLabelsThunk = createThunk(
txid,
}: {
labelsToBeEdited: RbfLabelsToBeUpdated;
precomposedTransaction: PrecomposedTransactionFinalRbf;
precomposedTransaction: PrecomposedTransactionFinalBumpFeeRbf;
txid: string;
},
{ dispatch },
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/suite/src/actions/wallet/stakeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { PrecomposedTransactionFinal, StakeFormState, StakeType } from '@suite-common/wallet-types';
import {
formatNetworkAmount,
isRbfTransaction,
isRbfBumpFeeTransaction,
isSupportedEthStakingNetworkSymbol,
isSupportedSolStakingNetworkSymbol,
tryGetAccountIdentity,
Expand Down Expand Up @@ -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.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { FormState, StakeFormState } from '@suite-common/wallet-types';
import {
constructTransactionReviewOutputs,
getTxStakeNameByDataHex,
isRbfTransaction,
isRbfBumpFeeTransaction,
isRbfCancelTransaction,
} from '@suite-common/wallet-utils';
import { NewModal } from '@trezor/components';
import { copyToClipboard, download } from '@trezor/dom-utils';
Expand Down Expand Up @@ -70,10 +71,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),
Expand Down Expand Up @@ -112,9 +116,12 @@ export const TransactionReviewModalContent = ({
}
};

const isCancelRbfAction = isRbfCancelTransaction(precomposedTx);

const actionLabel = getTransactionReviewModalActionText({
stakeType,
isRbfAction,
isBumpFeeRbfAction,
isCancelRbfAction,
isSending,
});

Expand Down Expand Up @@ -148,7 +155,7 @@ export const TransactionReviewModalContent = ({
}
if (decision) {
decision.resolve(true);
reportTransactionCreatedEvent(isRbfAction ? 'replaced' : 'sent');
reportTransactionCreatedEvent(isBumpFeeRbfAction ? 'replaced' : 'sent');
}
};

Expand Down Expand Up @@ -223,7 +230,11 @@ export const TransactionReviewModalContent = ({
}

if (isRbfConfirmedError) {
return <ReplaceByFeeFailedOriginalTxConfirmed type="replace-by-fee" />;
return (
<ReplaceByFeeFailedOriginalTxConfirmed
type={isCancelRbfAction ? 'cancel-transaction' : 'replace-by-fee'}
/>
);
}

return (
Expand All @@ -233,7 +244,7 @@ export const TransactionReviewModalContent = ({
signedTx={serializedTx}
outputs={outputs}
buttonRequestsCount={buttonRequestsCount}
isRbfAction={isRbfAction}
isRbfAction={isBumpFeeRbfAction}
isTradingAction={isTradingAction}
isSending={isSending}
stakeType={stakeType || undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,7 +50,8 @@ export const CancelTransactionModal = ({
const { account } = selectedAccount;

const dispatch = useDispatch();
const [composedCancelTx, setComposedCancelTx] = useState<PrecomposeResultFinal | null>(null);
const [composedCancelTx, setComposedCancelTx] =
useState<PrecomposedTransactionFinalCancelRbf | null>(null);

const confirmations = useSelector(state =>
selectTransactionConfirmations(state, tx.txid, account.key),
Expand All @@ -69,7 +70,12 @@ export const CancelTransactionModal = ({

dispatch(composeCancelTransactionThunk({ account, tx, chainedTxs }))
.unwrap()
.then(setComposedCancelTx)
.then(precomposed => {
setComposedCancelTx({
...precomposed,
prevTxToBeCanceledId: tx.txid,
});
})
.catch(setError);
}, [account, tx, dispatch, chainedTxs]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
import { MiddlewareAPI } from 'redux';

import { transactionsActions } from '@suite-common/wallet-core/';
import { isRbfTransaction } from '@suite-common/wallet-utils';
import { GeneralPrecomposedTransactionFinal } from '@suite-common/wallet-types';
import { isRbfBumpFeeTransaction, isRbfCancelTransaction } from '@suite-common/wallet-utils';

import { Action, AppState, Dispatch } from 'src/types/suite';

import { replaceByFeeErrorThunk } from '../../actions/wallet/send/replaceByFeeErrorThunk';

const getPrevTxid = (tx: GeneralPrecomposedTransactionFinal) => {
if (isRbfBumpFeeTransaction(tx)) {
return tx.prevTxid;
}

if (isRbfCancelTransaction(tx)) {
return tx.prevTxToBeCanceledId;
}

return null;
};

export const replaceByFeeErrorMiddleware =
(api: MiddlewareAPI<Dispatch, AppState>) =>
(next: Dispatch) =>
(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;

if (precomposedTx === undefined) {
return action;
}

const precomposedTx = api.getState().wallet.send?.precomposedTx;
const prevTxId = getPrevTxid(precomposedTx);
if (prevTxId === null) {
return action;
}

if (precomposedTx !== undefined && isRbfTransaction(precomposedTx)) {
const addedTransaction = transactions.find(
tx => tx.txid === precomposedTx.prevTxid,
);
const addedTransaction = transactions.find(tx => tx.txid === prevTxId);

if (addedTransaction !== undefined && addedTransaction.blockHeight !== undefined) {
api.dispatch(replaceByFeeErrorThunk());
}
}
if (addedTransaction !== undefined && addedTransaction.blockHeight !== undefined) {
api.dispatch(replaceByFeeErrorThunk());
}

return action;
Expand Down
12 changes: 9 additions & 3 deletions packages/suite/src/utils/suite/transactionReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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';
}
Expand Down
4 changes: 2 additions & 2 deletions suite-common/wallet-core/src/send/sendFormBitcoinThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getBitcoinComposeOutputs,
getUtxoOutpoint,
hasNetworkFeatures,
isRbfTransaction,
isRbfBumpFeeTransaction,
restoreOrigOutputsOrder,
} from '@suite-common/wallet-utils';
import TrezorConnect, {
Expand Down Expand Up @@ -273,7 +273,7 @@ export const signBitcoinSendFormTransactionThunk = createThunk<

if (
formState.rbfParams &&
isRbfTransaction(precomposedTransaction) &&
isRbfBumpFeeTransaction(precomposedTransaction) &&
precomposedTransaction.useNativeRbf
) {
const { txid, utxo, outputs } = formState.rbfParams;
Expand Down
10 changes: 5 additions & 5 deletions suite-common/wallet-core/src/send/sendFormThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
PrecomposedLevels,
PrecomposedLevelsCardano,
PrecomposedTransactionFinal,
PrecomposedTransactionFinalBumpFeeRbf,
PrecomposedTransactionFinalCardano,
PrecomposedTransactionFinalRbf,
} from '@suite-common/wallet-types';
import {
amountToSmallestUnit,
Expand Down Expand Up @@ -539,16 +539,16 @@ export const enhancePrecomposedTransactionThunk = createThunk<

if (!isCardanoTx(selectedAccount, enhancedPrecomposedTransaction)) {
if (formValues.rbfParams) {
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).prevTxid =
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalBumpFeeRbf).prevTxid =
formValues.rbfParams.txid;
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).feeDifference =
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalBumpFeeRbf).feeDifference =
new BigNumber(precomposedTransaction.fee)
.minus(formValues.rbfParams.baseFee)
.toFixed();
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf).useNativeRbf =
(enhancedPrecomposedTransaction as PrecomposedTransactionFinalBumpFeeRbf).useNativeRbf =
!!useNativeRbf;
(
enhancedPrecomposedTransaction as PrecomposedTransactionFinalRbf
enhancedPrecomposedTransaction as PrecomposedTransactionFinalBumpFeeRbf
).useDecreaseOutput = !!hasDecreasedOutput;
}
}
Expand Down
10 changes: 5 additions & 5 deletions suite-common/wallet-core/src/transactions/transactionsThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
AccountKey,
PrecomposedTransactionCardanoFinal,
PrecomposedTransactionFinal,
PrecomposedTransactionFinalRbf,
PrecomposedTransactionFinalBumpFeeRbf,
Timestamp,
WalletAccountTransaction,
} from '@suite-common/wallet-types';
Expand All @@ -15,7 +15,7 @@ import {
findTransactions,
getPendingAccount,
getRbfParams,
isRbfTransaction,
isRbfBumpFeeTransaction,
isTrezorConnectBackendType,
replaceEthereumSpecific,
tryGetAccountIdentity,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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());
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions suite-common/wallet-types/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,23 @@ export type PrecomposedTransactionCardanoFinal =
token?: TokenInfo;
};

export type PrecomposedTransactionFinalRbf = TxFinal & {
export type PrecomposedTransactionFinalBumpFeeRbf = TxFinal & {
prevTxid: string;
feeDifference: string;
// Native RBF is a firmware feature to recognize an RBF transaction and simplify transaction review flow.
useNativeRbf: boolean;
useDecreaseOutput: boolean;
};

// strict distinction between normal and RBF type
export type PrecomposedTransactionFinal = TxFinal | PrecomposedTransactionFinalRbf;
export type PrecomposedTransactionFinalCancelRbf = TxFinal & {
prevTxToBeCanceledId: string;
};

// Strict distinction between Normal-Tx and Bump-Fee-Tx / Cancel-Tx
export type PrecomposedTransactionFinal =
| TxFinal
| PrecomposedTransactionFinalBumpFeeRbf
| PrecomposedTransactionFinalCancelRbf;

export type PrecomposedTransaction =
| PrecomposedTransactionError
Expand All @@ -142,6 +149,7 @@ export type PrecomposedTransactionCardano =
| PrecomposedTransactionCardanoFinal;

export type GeneralPrecomposedTransaction = PrecomposedTransaction | PrecomposedTransactionCardano;

export type GeneralPrecomposedTransactionFinal = Extract<
GeneralPrecomposedTransaction,
{ type: 'final' }
Expand Down
Loading

0 comments on commit f1a775a

Please sign in to comment.