Skip to content

Commit

Permalink
feat(suite): add timer to solana tx modal
Browse files Browse the repository at this point in the history
  • Loading branch information
izmy authored and tomasklim committed Feb 25, 2025
1 parent f0b175c commit e409a3b
Show file tree
Hide file tree
Showing 22 changed files with 446 additions and 78 deletions.
13 changes: 13 additions & 0 deletions packages/blockchain-link/src/workers/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
RpcMainnet,
RpcSubscriptionsMainnet,
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED,
SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_FAILED_TO_CONNECT,
SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR,
SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND,
Signature,
Slot,
SolanaRpcApiMainnet,
Expand Down Expand Up @@ -196,6 +198,17 @@ const pushTransaction = async (request: Request<MessageTypes.PushTransaction>) =
'Solana backend connection failure. The backend might be inaccessible or the connection is unstable.',
);
}
if (
isSolanaError(
error,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
) &&
isSolanaError(error.cause, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND)
) {
throw new Error(
'The transaction has expired because too much time passed between signing and sending. Please try again.',
);
}
if (isSolanaError(error)) {
throw new Error(
`Solana error code: ${error.context.__code}. Please try again or contact support.`,
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/components/Badge/Badge.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Badge: StoryObj<BadgeProps> = {
'primary',
'tertiary',
'destructive',
'warning',
undefined,
] satisfies BadgeProps['variant'][],
},
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type BadgeSize = Extract<UISize, (typeof badgeSizes)[number]>;
export const allowedBadgeFrameProps = ['margin'] as const satisfies FramePropsKeys[];
type AllowedFrameProps = Pick<FrameProps, (typeof allowedBadgeFrameProps)[number]>;

export type BadgeVariant = Extract<UIVariant, 'primary' | 'tertiary' | 'destructive'>;
export type BadgeVariant = Extract<UIVariant, 'primary' | 'tertiary' | 'destructive' | 'warning'>;

export type BadgeProps = AllowedFrameProps & {
size?: BadgeSize;
Expand Down Expand Up @@ -54,6 +54,7 @@ const mapVariantToBackgroundColor = ({ $variant, $onElevation, theme }: MapArgs)
primary: 'backgroundPrimarySubtleOnElevation0',
tertiary: `backgroundNeutralSubtleOnElevation${$onElevation ? 1 : 0}`,
destructive: 'backgroundAlertRedSubtleOnElevation0',
warning: 'backgroundAlertYellowSubtleOnElevation0',
};

return theme[colorMap[$variant]];
Expand All @@ -64,6 +65,7 @@ const mapVariantToTextColor = ({ $variant, theme }: MapArgs): CSSColor => {
primary: 'textPrimaryDefault',
tertiary: 'textSubdued',
destructive: 'textAlertRed',
warning: 'textAlertYellow',
};

return theme[colorMap[$variant]];
Expand All @@ -74,6 +76,7 @@ const mapVariantToIconColor = ({ $variant, theme }: MapArgs): CSSColor => {
primary: 'iconPrimaryDefault',
tertiary: 'iconSubdued',
destructive: 'iconAlertRed',
warning: 'iconAlertYellow',
};

return theme[colorMap[$variant]];
Expand Down
2 changes: 1 addition & 1 deletion packages/suite-data/files/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1681,7 +1681,7 @@
"TR_STAKE_ETH_REWARDS_EARN": "Your rewards also earn. Keep them staked and watch your {networkDisplaySymbol} rewards soar.",
"TR_STAKE_ETH_REWARDS_EARN_APY": "Your {networkDisplaySymbol} rewards also earn the APY rate. Keep your funds staked or add more to increase your rewards.",
"TR_STAKE_ETH_SEE_MONEY_DANCE": "Watch your money dance",
"TR_STAKE_ETH_WILL_BE_BLOCKED": "Your {networkDisplaySymbol} will be blocked during this period and can't be canceled. <a>Learn more</a>",
"TR_STAKE_ETH_WILL_BE_BLOCKED": "Your {networkDisplaySymbol} will be blocked during this period and can't be canceled.",
"TR_STAKE_EVERSTAKE_MANAGES": "Everstake maintains and protects your staked {networkDisplaySymbol} <t>with their smart contracts, infrastructure, and technology.</t>",
"TR_STAKE_EXPECTED_REWARDS": "Expected rewards per 1 epoch (up to {count, plural, one {# day} other {# days}})",
"TR_STAKE_INSTANT": "Instant",
Expand Down
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
30 changes: 19 additions & 11 deletions packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ export const signTransaction =
!device ||
!transactionInfo ||
transactionInfo.type !== 'final'
)
) {
return;
}

const { account } = selectedAccount;
if (account.networkType !== 'solana') return;
if (account.networkType !== 'solana') {
return;
}

const addressDisplayType = selectAddressDisplayType(getState());
const estimatedFee = {
Expand Down Expand Up @@ -251,15 +254,20 @@ export const signTransaction =

if (!signedTx.success) {
// catch manual error from TransactionReviewModal
if (signedTx.payload.error === 'tx-cancelled') return;
dispatch(
notificationsActions.addToast({
type: 'sign-tx-error',
error: signedTx.payload.error,
}),
);

return;
if (signedTx.payload.error === 'tx-cancelled') {
return;
}

if (signedTx.payload.error !== 'tx-timeout') {
dispatch(
notificationsActions.addToast({
type: 'sign-tx-error',
error: signedTx.payload.error,
}),
);
}

return signedTx;
}

txData.tx.txShim.addSignature(address(account.descriptor), signedTx.payload.signature);
Expand Down
9 changes: 6 additions & 3 deletions packages/suite/src/actions/wallet/stakeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
isSupportedSolStakingNetworkSymbol,
tryGetAccountIdentity,
} from '@suite-common/wallet-utils';
import TrezorConnect from '@trezor/connect';
import TrezorConnect, { Unsuccessful } from '@trezor/connect';
import { BigNumber } from '@trezor/utils/src/bigNumber';

import { Dispatch, GetState } from 'src/types/suite';
Expand Down Expand Up @@ -178,7 +178,7 @@ export const signTransaction =
dispatch(modalActions.preserve());

// signTransaction by Trezor
let serializedTx: string | undefined;
let serializedTx: undefined | string | Unsuccessful;
if (isSupportedEthStakingNetworkSymbol(account.symbol)) {
serializedTx = await dispatch(
stakeFormEthereumActions.signTransaction(formValues, enhancedTxInfo),
Expand All @@ -191,7 +191,10 @@ export const signTransaction =
);
}

if (!serializedTx) {
if (typeof serializedTx !== 'string') {
if (serializedTx?.payload?.error === 'tx-timeout') {
return;
}
// close modal manually since UI.CLOSE_UI.WINDOW was blocked
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,7 +1,17 @@
import { UserContextPayload } from '@suite-common/suite-types';
import { cancelSignSendFormTransactionThunk, selectStake } from '@suite-common/wallet-core';
import {
cancelSignSendFormTransactionThunk,
selectStake,
sendFormActions,
stakeActions,
} from '@suite-common/wallet-core';
import { PrecomposedTransactionFinal } from '@suite-common/wallet-types';

import { cancelSignTx as cancelSignStakingTx } from 'src/actions/wallet/stakeActions';
import { signAndPushSendFormTransactionThunk } from 'src/actions/wallet/send/sendFormThunks';
import {
cancelSignTx as cancelSignStakingTx,
signTransaction,
} from 'src/actions/wallet/stakeActions';
import { useDispatch, useSelector } from 'src/hooks/suite';

import { TransactionReviewModalContent } from './TransactionReviewModalContent';
Expand All @@ -19,21 +29,48 @@ type TransactionReviewModalProps =
export const TransactionReviewModal = ({ type, decision }: TransactionReviewModalProps) => {
const send = useSelector(state => state.wallet.send);
const stake = useSelector(selectStake);
const selectedAccount = useSelector(state => state.wallet.selectedAccount);
const dispatch = useDispatch();

const isSend = Boolean(send?.precomposedTx);
// Only one state should be available when the modal is open
const txInfoState = isSend ? send : stake;

const handleCancelSignTx = () => {
if (isSend) dispatch(cancelSignSendFormTransactionThunk());
else dispatch(cancelSignStakingTx());
if (isSend) {
dispatch(cancelSignSendFormTransactionThunk());
} else {
dispatch(cancelSignStakingTx());
}
};

const handleTryAgainSignTx = () => {
if (send.precomposedForm && send.precomposedTx) {
dispatch(sendFormActions.discardTransaction());
dispatch(
signAndPushSendFormTransactionThunk({
formState: send.precomposedForm,
precomposedTransaction: send.precomposedTx,
selectedAccount: selectedAccount.account,
}),
);
} else if (stake.precomposedForm && stake.precomposedTx) {
dispatch(stakeActions.dispose());
dispatch(
signTransaction(
stake.precomposedForm,
stake.precomposedTx as PrecomposedTransactionFinal,
),
);
}
};

return (
<TransactionReviewModalContent
timestamp={txInfoState.precomposedForm?.createdTimestamp}
decision={decision}
txInfoState={txInfoState}
tryAgainSignTx={handleTryAgainSignTx}
cancelSignTx={handleCancelSignTx}
isRbfConfirmedError={type === 'review-transaction-rbf-previous-transaction-mined-error'}
/>
Expand Down
Loading

0 comments on commit e409a3b

Please sign in to comment.