Skip to content

Commit

Permalink
fixup! feat(suite): add timer to solana tx modal
Browse files Browse the repository at this point in the history
  • Loading branch information
izmy committed Feb 21, 2025
1 parent 65579aa commit f2c6dd9
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 87 deletions.
5 changes: 5 additions & 0 deletions packages/suite/src/actions/wallet/send/sendFormThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
1 change: 1 addition & 0 deletions packages/suite/src/components/suite/CountdownTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const CountdownTimer = ({
id={messageId}
values={{
value: getValue(),
strong: chunks => <strong>{chunks}</strong>,
firstValue,
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -225,7 +277,7 @@ export const TransactionReviewModalContent = ({
);
}

if (isSolanaExpired) {
if (solanaExpired) {
return (
<>
<NewModal.Button variant="primary" onClick={() => handleTryAgain(false)}>
Expand Down Expand Up @@ -289,7 +341,7 @@ export const TransactionReviewModalContent = ({
);
}

if (isSolanaExpired) {
if (solanaExpired) {
return <ExpiredBlockhash symbol={symbol} />;
}

Expand All @@ -305,7 +357,7 @@ export const TransactionReviewModalContent = ({
isTradingAction={isTradingAction}
isSending={isSending}
stakeType={stakeType || undefined}
remainingTime={remainingTime}
deadline={deadline}
onTryAgain={handleTryAgain}
/>
</Column>
Expand Down Expand Up @@ -340,25 +392,20 @@ export const TransactionReviewModalContent = ({
}}
stakeType={stakeType}
/>
{networkType === 'solana' &&
!isSolanaExpired &&
buttonRequestsCount != 1 && (
<Row gap={spacings.xs}>
<Button
icon="arrowClockwise"
variant="tertiary"
type="button"
size="tiny"
onClick={() => handleTryAgain(true)}
>
<Translation id="TR_AGAIN" />
</Button>
<TransactionReviewOutputTimer
remainingTime={remainingTime}
isMinimal
/>
</Row>
)}
{showSolanaTimer && (
<Row gap={spacings.xs}>
<Button
icon="arrowClockwise"
variant="tertiary"
type="button"
size="tiny"
onClick={() => handleTryAgain(true)}
>
<Translation id="TR_AGAIN" />
</Button>
<TransactionReviewOutputTimer deadline={deadline} isMinimal />
</Row>
)}
</Row>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type TransactionReviewOutputListProps = {
isTradingAction: boolean;
isSending?: boolean;
stakeType?: StakeType;
remainingTime?: number;
deadline?: number;
onTryAgain?: (close: boolean) => void;
};

Expand Down Expand Up @@ -76,7 +76,7 @@ export const TransactionReviewOutputList = ({
isTradingAction,
isSending,
stakeType,
remainingTime,
deadline,
onTryAgain,
}: TransactionReviewOutputListProps) => {
const outputRefs = useRef<(HTMLDivElement | null)[]>([]);
Expand Down Expand Up @@ -118,9 +118,9 @@ export const TransactionReviewOutputList = ({
<H3>
<Translation id="TR_SEND_ADDRESS_CONFIRMATION_HEADING" />
</H3>
{networkType === 'solana' && remainingTime != null && (
{networkType === 'solana' && deadline && (
<TransactionReviewOutputTimer
remainingTime={remainingTime}
deadline={deadline}
onTryAgain={onTryAgain}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -21,37 +22,41 @@ 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 (
<Badge variant="warning">
<TimerBox>
<Translation
id="TR_SOLANA_TX_CONFIRMATION_TIMER_SHORT"
values={{
remainingTime,
strong: chunks => <strong>{chunks}</strong>,
}}
<CountdownTimer
deadline={deadline}
unitDisplay="narrow"
message="TR_SOLANA_TX_CONFIRMATION_TIMER_SHORT"
pastDeadlineMessage="TR_SOLANA_TX_SEND_FAILED_TITLE"
/>
</TimerBox>
</Badge>
);
}

return (
<Banner variant="warning" icon="hourglass">
<Banner icon="hourglass">
<TimerBox>
<Text typographyStyle="callout" as="div">
<Translation id="TR_SOLANA_TX_CONFIRMATION_TIMER" values={{ remainingTime }} />
<CountdownTimer
deadline={deadline}
unitDisplay="long"
message="TR_SOLANA_TX_CONFIRMATION_TIMER"
pastDeadlineMessage="TR_SOLANA_TX_SEND_FAILED_TITLE"
/>
</Text>
<Translation id="TR_SOLANA_TX_CONFIRMATION_TIMER_DESCRIPTION" />
{onTryAgain && (
Expand Down
37 changes: 0 additions & 37 deletions packages/suite/src/hooks/useTransactionTimeout.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<strong>{remainingTime}s</strong> left to confirm',
defaultMessage: '<strong>{value}</strong> left to confirm',
},
TR_SOLANA_TX_CONFIRMATION_TIMER_DESCRIPTION: {
id: 'TR_SOLANA_TX_CONFIRMATION_TIMER_DESCRIPTION',
Expand Down
8 changes: 6 additions & 2 deletions suite-common/wallet-core/src/send/sendFormThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
ComposeFeeLevelsError,
PushTransactionError,
SignTransactionError,
SignTransactionTimeoutError,
} from './sendFormTypes';
import { accountsActions } from '../accounts/accountsActions';
import { selectAccountByKey } from '../accounts/accountsReducer';
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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') {
Expand Down
Loading

0 comments on commit f2c6dd9

Please sign in to comment.