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

feat: extend reporting for 'canceled' event in sendform
  • Loading branch information
peter-sanderson committed Feb 11, 2025
1 parent 1516375 commit 817360d
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 105 deletions.
36 changes: 19 additions & 17 deletions packages/suite-analytics/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
| {
Expand Down Expand Up @@ -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: {
Expand Down
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 @@ -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';
Expand All @@ -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<boolean, string | number | undefined> | undefined;
txInfoState: SendState | StakeState;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -148,7 +164,11 @@ export const TransactionReviewModalContent = ({
}
if (decision) {
decision.resolve(true);
reportTransactionCreatedEvent(isRbfAction ? 'replaced' : 'sent');
reportTransactionCreatedEvent(
isRbfTransaction(precomposedTx)
? mapRbfTypeToReporting[precomposedTx.rbfType]
: 'sent',
);
}
};

Expand Down Expand Up @@ -222,8 +242,8 @@ export const TransactionReviewModalContent = ({
return <TransactionReviewDetails tx={precomposedTx} txHash={serializedTx?.tx} />;
}

if (isRbfConfirmedError) {
return <ReplaceByFeeFailedOriginalTxConfirmed type="replace-by-fee" />;
if (isRbfConfirmedError && isRbfTransaction(precomposedTx)) {
return <ReplaceByFeeFailedOriginalTxConfirmed type={precomposedTx.rbfType} />;
}

return (
Expand All @@ -233,7 +253,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,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]);

Expand Down Expand Up @@ -100,7 +103,7 @@ export const CancelTransactionModal = ({
onBackClick={onBackClick}
>
{isTxConfirmed ? (
<ReplaceByFeeFailedOriginalTxConfirmed type="cancel-transaction" />
<ReplaceByFeeFailedOriginalTxConfirmed type="cancel" />
) : (
<Column gap={spacings.md}>
<CancelTransaction tx={tx} selectedAccount={selectedAccount} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const BumpFeeModal = ({
onBackClick={onBackClick}
>
{isTxConfirmed ? (
<ReplaceByFeeFailedOriginalTxConfirmed type="replace-by-fee" />
<ReplaceByFeeFailedOriginalTxConfirmed type="bump-fee" />
) : (
<ChangeFee tx={tx} chainedTxs={chainedTxs} showChained={onShowChained} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<ReplaceByFeeFailedOriginalTxConfirmedProps['type'], TranslationKey> = {
'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<ReplaceByFeeFailedOriginalTxConfirmedProps['type'], TranslationKey> = {
'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<ReplaceByFeeFailedOriginalTxConfirmedProps['type'], Url> = {
'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 = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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
Loading

0 comments on commit 817360d

Please sign in to comment.