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

Solana tx timer #17045

Merged
merged 3 commits into from
Feb 27, 2025
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
18 changes: 16 additions & 2 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 Expand Up @@ -422,9 +435,10 @@ const getAccountInfo = async (

const getInfo = async (request: Request<MessageTypes.GetInfo>, isTestnet: boolean) => {
const api = await request.connect();

const {
value: { blockhash: blockHash, lastValidBlockHeight: blockHeight },
} = await api.rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
} = await api.rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();

const serverInfo = {
testnet: isTestnet,
Expand All @@ -434,7 +448,7 @@ const getInfo = async (request: Request<MessageTypes.GetInfo>, isTestnet: boolea
network: isTestnet ? 'dsol' : 'sol',
url: api.clusterUrl,
name: 'Solana',
version: (await api.rpc.getVersion().send())['solana-core'],
version: '1', // saving request api.rpc.getVersion().send(), version is not used anyways
decimals: 9,
};

Expand Down
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
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,47 @@ 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
decision={decision}
txInfoState={txInfoState}
tryAgainSignTx={handleTryAgainSignTx}
cancelSignTx={handleCancelSignTx}
isRbfConfirmedError={type === 'review-transaction-rbf-previous-transaction-mined-error'}
/>
Expand Down
Loading