diff --git a/packages/suite/src/actions/wallet/send/sendFormThunks.ts b/packages/suite/src/actions/wallet/send/sendFormThunks.ts
index 37dd4476612..4cdb809847e 100644
--- a/packages/suite/src/actions/wallet/send/sendFormThunks.ts
+++ b/packages/suite/src/actions/wallet/send/sendFormThunks.ts
@@ -232,6 +232,11 @@ export const signAndPushSendFormTransactionThunk = createThunk(
return;
}
+ // Do not close the modal if the transaction signing timed out
+ if (signResponse.payload?.error === 'sign-transaction-timeout') {
+ return;
+ }
+
// Close the modal manually since UI.CLOSE_UI.WINDOW was
// blocked by `modalActions.preserve` above.
dispatch(modalActions.onCancel());
diff --git a/packages/suite/src/components/suite/CountdownTimer.tsx b/packages/suite/src/components/suite/CountdownTimer.tsx
index 1967741247d..809e33287aa 100644
--- a/packages/suite/src/components/suite/CountdownTimer.tsx
+++ b/packages/suite/src/components/suite/CountdownTimer.tsx
@@ -85,6 +85,7 @@ export const CountdownTimer = ({
id={messageId}
values={{
value: getValue(),
+ strong: chunks => {chunks},
firstValue,
}}
/>
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModal.tsx
index 177c39f0709..9922cd59c45 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModal.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModal.tsx
@@ -1,5 +1,9 @@
import { UserContextPayload } from '@suite-common/suite-types';
-import { cancelSignSendFormTransactionThunk, selectStake } from '@suite-common/wallet-core';
+import {
+ cancelSignSendFormTransactionThunk,
+ selectStake,
+ sendFormActions,
+} from '@suite-common/wallet-core';
import { signAndPushSendFormTransactionThunk } from 'src/actions/wallet/send/sendFormThunks';
import { cancelSignTx as cancelSignStakingTx } from 'src/actions/wallet/stakeActions';
@@ -34,6 +38,7 @@ export const TransactionReviewModal = ({ type, decision }: TransactionReviewModa
const handleTryAgainSignTx = () => {
if (send.precomposedForm != null && send.precomposedTx != null) {
+ dispatch(sendFormActions.discardTransaction());
dispatch(
signAndPushSendFormTransactionThunk({
formState: send.precomposedForm,
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 59fb4c1c62a..f48f318a372 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx
@@ -1,7 +1,9 @@
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { notificationsActions } from '@suite-common/toast-notifications';
+import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
import {
+ AccountsState,
DeviceRootState,
SendState,
StakeState,
@@ -10,9 +12,15 @@ import {
selectSendFormReviewButtonRequestsCount,
selectStakePrecomposedForm,
} from '@suite-common/wallet-core';
-import { FormState, RbfTransactionType, StakeFormState } from '@suite-common/wallet-types';
+import {
+ FormState,
+ RbfTransactionType,
+ ReviewOutput,
+ StakeFormState,
+} from '@suite-common/wallet-types';
import {
constructTransactionReviewOutputs,
+ findAccountsByAddress,
getTxStakeNameByDataHex,
isRbfBumpFeeTransaction,
isRbfCancelTransaction,
@@ -29,7 +37,6 @@ import { Deferred } from '@trezor/utils';
import * as modalActions from 'src/actions/suite/modalActions';
import { Translation } from 'src/components/suite';
import { useDispatch, useSelector } from 'src/hooks/suite';
-import { useTransactionTimeout } from 'src/hooks/useTransactionTimeout';
import { selectIsActionAbortable } from 'src/reducers/suite/suiteReducer';
import { selectAccountIncludingChosenInTrading } from 'src/reducers/wallet/selectedAccountReducer';
import { getTransactionReviewModalActionText } from 'src/utils/suite/transactionReview';
@@ -42,7 +49,32 @@ import { ConfirmActionModal } from '../DeviceContextModal/ConfirmActionModal';
import { ExpiredBlockhash } from '../UserContextModal/TxDetailModal/ExpiredBlockhash';
import { ReplaceByFeeFailedOriginalTxConfirmed } from '../UserContextModal/TxDetailModal/ReplaceByFeeFailedOriginalTxConfirmed';
-const SOLANA_TX_TIMEOUT_SECONDS = 58;
+const SOLANA_TX_TIMEOUT_MS = 60 * 1000;
+
+const isSolanaExpired = (networkType: NetworkType, deadline: number) =>
+ networkType === 'solana' && deadline <= Date.now();
+
+const shouldShowSolanaTimer = (
+ networkType: NetworkType,
+ deadline: number,
+ outputs: ReviewOutput[],
+ symbol: NetworkSymbol,
+ accounts: AccountsState,
+ buttonRequestsCount: number,
+) => {
+ if (networkType !== 'solana' || isSolanaExpired(networkType, deadline)) {
+ return false;
+ }
+
+ const firstOutput = outputs[0];
+ const isInternalTransfer =
+ firstOutput?.type === 'address' &&
+ findAccountsByAddress(symbol, firstOutput.value, accounts).length > 0;
+
+ const isFirstStep = buttonRequestsCount === 1;
+
+ return isInternalTransfer || !isFirstStep;
+};
const isStakeState = (state: SendState | StakeState): state is StakeState => 'data' in state;
@@ -76,14 +108,25 @@ export const TransactionReviewModalContent = ({
}: TransactionReviewModalContentProps) => {
const dispatch = useDispatch();
const account = useSelector(selectAccountIncludingChosenInTrading);
+ const accounts = useSelector(state => state.wallet.accounts);
const device = useSelector(selectSelectedDevice);
const isActionAbortable = useSelector(selectIsActionAbortable);
const [isSending, setIsSending] = useState(false);
const [areDetailsVisible, setAreDetailsVisible] = useState(false);
- const remainingTime = useTransactionTimeout(timestamp, SOLANA_TX_TIMEOUT_SECONDS, () => {
- TrezorConnect.cancel('tx-timeout');
- });
+ const deadline = timestamp + SOLANA_TX_TIMEOUT_MS;
+
+ // check if transaction is still valid
+ useEffect(() => {
+ const now = Date.now();
+ const timeLeft = Math.max(deadline - now, 0);
+
+ const timeoutId = setTimeout(() => {
+ TrezorConnect.cancel('tx-timeout');
+ }, timeLeft);
+
+ return () => clearTimeout(timeoutId);
+ }, [deadline]);
const deviceModelInternal = device?.features?.internal_model;
const { precomposedTx, serializedTx } = txInfoState;
@@ -142,7 +185,16 @@ export const TransactionReviewModalContent = ({
const isCancelRbfAction = isRbfCancelTransaction(precomposedTx);
- const isSolanaExpired = networkType === 'solana' && remainingTime <= 0;
+ const solanaExpired = isSolanaExpired(networkType, deadline);
+
+ const showSolanaTimer = shouldShowSolanaTimer(
+ networkType,
+ deadline,
+ outputs,
+ symbol,
+ accounts,
+ buttonRequestsCount,
+ );
const actionLabel = getTransactionReviewModalActionText({
stakeType,
@@ -225,7 +277,7 @@ export const TransactionReviewModalContent = ({
);
}
- if (isSolanaExpired) {
+ if (solanaExpired) {
return (
<>
handleTryAgain(false)}>
@@ -289,7 +341,7 @@ export const TransactionReviewModalContent = ({
);
}
- if (isSolanaExpired) {
+ if (solanaExpired) {
return ;
}
@@ -305,7 +357,7 @@ export const TransactionReviewModalContent = ({
isTradingAction={isTradingAction}
isSending={isSending}
stakeType={stakeType || undefined}
- remainingTime={remainingTime}
+ deadline={deadline}
onTryAgain={handleTryAgain}
/>
@@ -340,25 +392,20 @@ export const TransactionReviewModalContent = ({
}}
stakeType={stakeType}
/>
- {networkType === 'solana' &&
- !isSolanaExpired &&
- buttonRequestsCount != 1 && (
-
-
-
-
- )}
+ {showSolanaTimer && (
+
+
+
+
+ )}
)
}
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx
index 8e37d2ad9a7..647662066c1 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx
@@ -27,7 +27,7 @@ export type TransactionReviewOutputListProps = {
isTradingAction: boolean;
isSending?: boolean;
stakeType?: StakeType;
- remainingTime?: number;
+ deadline?: number;
onTryAgain?: (close: boolean) => void;
};
@@ -76,7 +76,7 @@ export const TransactionReviewOutputList = ({
isTradingAction,
isSending,
stakeType,
- remainingTime,
+ deadline,
onTryAgain,
}: TransactionReviewOutputListProps) => {
const outputRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -118,9 +118,9 @@ export const TransactionReviewOutputList = ({
- {networkType === 'solana' && remainingTime != null && (
+ {networkType === 'solana' && deadline && (
)}
diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputTimer.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputTimer.tsx
index 08774a8cae6..6ae6ef9917e 100644
--- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputTimer.tsx
+++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputTimer.tsx
@@ -5,6 +5,7 @@ import styled from 'styled-components';
import { Badge, Banner, Row, Text } from '@trezor/components';
import { spacings } from '@trezor/theme';
+import { CountdownTimer } from 'src/components/suite/CountdownTimer';
import { Translation } from 'src/components/suite/Translation';
const TimerBox = styled.div`
@@ -21,26 +22,25 @@ const RetryLink = styled.a`
`;
type TransactionReviewOutputTimerProps = {
- remainingTime: number;
+ deadline: number;
isMinimal?: boolean;
onTryAgain?: (close: boolean) => void;
};
export const TransactionReviewOutputTimer = ({
+ deadline,
isMinimal,
- remainingTime,
onTryAgain,
}: TransactionReviewOutputTimerProps) => {
if (isMinimal) {
return (
- {chunks},
- }}
+
@@ -48,10 +48,15 @@ export const TransactionReviewOutputTimer = ({
}
return (
-
+
-
+
{onTryAgain && (
diff --git a/packages/suite/src/hooks/useTransactionTimeout.tsx b/packages/suite/src/hooks/useTransactionTimeout.tsx
deleted file mode 100644
index 64c2c0041b0..00000000000
--- a/packages/suite/src/hooks/useTransactionTimeout.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-
-const getRemainingTime = (timestamp: number, timeoutSeconds: number) => {
- const elapsedTime = Math.floor((Date.now() - timestamp) / 1000);
-
- return Math.max(0, timeoutSeconds - elapsedTime);
-};
-
-export const useTransactionTimeout = (
- timestamp: number,
- timeoutSeconds: number,
- onTimeout?: () => void,
-) => {
- const [remainingTime, setRemainingTime] = useState(() =>
- getRemainingTime(timestamp, timeoutSeconds),
- );
-
- const handleTimeout = useCallback(() => {
- onTimeout?.();
- }, [onTimeout]);
-
- useEffect(() => {
- const intervalId = setInterval(() => {
- const newRemainingTime = getRemainingTime(timestamp, timeoutSeconds);
- setRemainingTime(newRemainingTime);
-
- if (newRemainingTime === 0) {
- clearInterval(intervalId);
- handleTimeout();
- }
- }, 1000);
-
- return () => clearInterval(intervalId);
- }, [timestamp, timeoutSeconds, handleTimeout]);
-
- return remainingTime;
-};
diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts
index 684b9a4963a..8f93bcd8207 100644
--- a/packages/suite/src/support/messages.ts
+++ b/packages/suite/src/support/messages.ts
@@ -9143,11 +9143,11 @@ export default defineMessages({
},
TR_SOLANA_TX_CONFIRMATION_TIMER: {
id: 'TR_SOLANA_TX_CONFIRMATION_TIMER',
- defaultMessage: '{remainingTime} seconds left',
+ defaultMessage: '{value} left',
},
TR_SOLANA_TX_CONFIRMATION_TIMER_SHORT: {
id: 'TR_SOLANA_TX_CONFIRMATION_TIMER_SHORT',
- defaultMessage: '{remainingTime}s left to confirm',
+ defaultMessage: '{value} left to confirm',
},
TR_SOLANA_TX_CONFIRMATION_TIMER_DESCRIPTION: {
id: 'TR_SOLANA_TX_CONFIRMATION_TIMER_DESCRIPTION',
diff --git a/suite-common/wallet-core/src/send/sendFormThunks.ts b/suite-common/wallet-core/src/send/sendFormThunks.ts
index 4cdd2793ab4..773bbd9b643 100644
--- a/suite-common/wallet-core/src/send/sendFormThunks.ts
+++ b/suite-common/wallet-core/src/send/sendFormThunks.ts
@@ -64,6 +64,7 @@ import {
ComposeFeeLevelsError,
PushTransactionError,
SignTransactionError,
+ SignTransactionTimeoutError,
} from './sendFormTypes';
import { accountsActions } from '../accounts/accountsActions';
import { selectAccountByKey } from '../accounts/accountsReducer';
@@ -399,7 +400,7 @@ export const signTransactionThunk = createThunk<
precomposedTransaction: PrecomposedTransactionFinal | PrecomposedTransactionFinalCardano;
selectedAccount: Account;
},
- { rejectValue: SignTransactionError | undefined }
+ { rejectValue: SignTransactionError | SignTransactionTimeoutError | undefined }
>(
`${SEND_MODULE_PREFIX}/signTransactionThunk`,
async (
@@ -458,7 +459,10 @@ export const signTransactionThunk = createThunk<
// catch manual error from TransactionReviewModal
const message = response?.payload?.message ?? 'unknown-error';
if (message === 'tx-timeout') {
- return;
+ return rejectWithValue({
+ error: 'sign-transaction-timeout',
+ message: 'Signing process timed out.',
+ });
}
if (message === 'tx-cancelled') {
diff --git a/suite-common/wallet-core/src/send/sendFormTypes.ts b/suite-common/wallet-core/src/send/sendFormTypes.ts
index 5a8a856c36b..319c0bbf38c 100644
--- a/suite-common/wallet-core/src/send/sendFormTypes.ts
+++ b/suite-common/wallet-core/src/send/sendFormTypes.ts
@@ -57,6 +57,11 @@ export type SignTransactionError = {
message?: string;
};
+export type SignTransactionTimeoutError = {
+ error: 'sign-transaction-timeout';
+ message?: string;
+};
+
export type PushTransactionError = {
error: 'push-transaction-failed';
metadata: Unsuccessful;
diff --git a/suite-native/module-send/src/sendFormThunks.ts b/suite-native/module-send/src/sendFormThunks.ts
index 370ae66929c..e11b6fdf24e 100644
--- a/suite-native/module-send/src/sendFormThunks.ts
+++ b/suite-native/module-send/src/sendFormThunks.ts
@@ -5,6 +5,7 @@ import { createThunk } from '@suite-common/redux-utils';
import { getNetwork } from '@suite-common/wallet-config';
import {
SignTransactionError,
+ SignTransactionTimeoutError,
composeSendFormTransactionFeeLevelsThunk,
deviceActions,
enhancePrecomposedTransactionThunk,
@@ -39,7 +40,7 @@ export const signTransactionNativeThunk = createThunk<
feeLevel: GeneralPrecomposedTransactionFinal;
tokenContract?: TokenAddress;
},
- { rejectValue: SignTransactionError | undefined }
+ { rejectValue: SignTransactionError | SignTransactionTimeoutError | undefined }
>(
`${SEND_MODULE_PREFIX}/signTransactionNativeThunk`,
async (