Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement distinction between BumpFee RBF & Cancel RBF -> utilise it to display error in case TX gets confirmed in cancel flow #16905

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: handling of undefined could be moved inside of the isRbfBumpFeeTransaction type guard.


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';
peter-sanderson marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading