Skip to content

Commit

Permalink
fix: verify tx request, handle expired
Browse files Browse the repository at this point in the history
  • Loading branch information
aulneau committed Jun 15, 2021
1 parent 306562e commit 6a6640c
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 18 deletions.
19 changes: 12 additions & 7 deletions src/common/hooks/transaction/use-transaction-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ import { TransactionErrorReason } from '@pages/transaction/transaction-error';
import BigNumber from 'bignumber.js';
import { TransactionTypes } from '@stacks/connect';
import { useTransactionFee } from '@common/hooks/transaction/use-transaction-fee';
import {
isUnauthorizedTransactionState,
transactionBroadcastErrorState,
} from '@store/transactions';
import { transactionBroadcastErrorState } from '@store/transactions';
import { transactionRequestValidationState } from '@store/transactions/requests';
import { useLoadable } from '@common/hooks/use-loadable';
import { useOrigin } from '@common/hooks/use-origin';

export function useTransactionError() {
const transactionRequest = useTransactionRequest();
const fee = useTransactionFee();
const contractInterface = useTransactionContractInterface();
const broadcastError = useRecoilValue(transactionBroadcastErrorState);
const isUnauthorizedTransaction = useRecoilValue(isUnauthorizedTransactionState);
const isValidTransaction = useLoadable(transactionRequestValidationState);
const origin = useOrigin();

const { currentAccount } = useWallet();
const balances = useFetchBalances();
return useMemo<TransactionErrorReason | void>(() => {
if (isUnauthorizedTransaction) return TransactionErrorReason.Unauthorized;
if (origin === false) return TransactionErrorReason.ExpiredRequest;
if (isValidTransaction.contents === false && !isValidTransaction.isLoading)
return TransactionErrorReason.Unauthorized;

if (!transactionRequest || balances.errorMaybe() || !currentAccount) {
return TransactionErrorReason.Generic;
Expand Down Expand Up @@ -57,6 +61,7 @@ export function useTransactionError() {
balances,
currentAccount,
transactionRequest,
isUnauthorizedTransaction,
isValidTransaction,
origin,
]);
}
6 changes: 2 additions & 4 deletions src/common/hooks/use-origin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useRecoilValue } from 'recoil';
import { getRequestOrigin, StorageKey } from '@common/storage';
import { requestTokenState } from '@store/transactions/requests';
import { requestTokenOriginState } from '@store/transactions/requests';

export function useOrigin() {
const requestToken = useRecoilValue(requestTokenState);
return requestToken ? getRequestOrigin(StorageKey.transactionRequests, requestToken) : null;
return useRecoilValue(requestTokenOriginState);
}
56 changes: 56 additions & 0 deletions src/common/hooks/use-scroll-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// https://github.com/moldy530/react-use-scroll-lock/blob/master/src/use-scroll-lock.ts
import { useEffect, useState } from 'react';

declare global {
// tslint:disable-next-line: interface-name
interface Window {
__useScrollLockStyle: string | undefined | null;
__useScrollLockInstances: Set<{}> | undefined | null;
}
}

let instances: Set<{}> = new Set();

if (typeof window !== 'undefined') {
// this is necessary because we may share instances of this file on a page so we store these globally
window.__useScrollLockInstances = window.__useScrollLockInstances || new Set<{}>();
instances = window.__useScrollLockInstances;
}

const registerInstance = (instance: {}) => {
if (instances.size === 0) {
setBodyOverflow(true);
}

instances.add(instance);
};

const unregisterInstance = (instance: {}) => {
instances.delete(instance);

if (instances.size === 0) {
setBodyOverflow(false);
}
};

const setBodyOverflow = (shouldLock: boolean) => {
if (shouldLock) {
document.body.classList.add('no-scroll');
} else {
document.body.classList.remove('no-scroll');
}
};

export const useScrollLock = (shouldLock: boolean) => {
// we generate a unique reference to the component that uses this thing
const [elementId] = useState({});

useEffect(() => {
if (shouldLock) {
registerInstance(elementId);
}

// Re-enable scrolling when component unmounts
return () => unregisterInstance(elementId);
}, [elementId, shouldLock]); // ensures effect is only run on mount, unmount, and on shouldLock change
};
8 changes: 8 additions & 0 deletions src/components/global-styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import { Global, css } from '@emotion/react';
const SizeStyles = css`
body {
display: flex;
&.no-scroll .main-content {
overflow: hidden;
pointer-events: none;
}
}
#actions-root {
flex-grow: 1;
display: flex;
min-height: 100vh;
}
.container-outer {
min-height: 100vh;
height: 100vh;
}
.mode__extension {
&,
body {
Expand Down
1 change: 1 addition & 0 deletions src/components/popup/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const PopupContainer: React.FC<PopupHomeProps> = memo(
flexDirection="column"
flexGrow={1}
className="main-content"
id="main-content"
as="main"
position="relative"
width="100%"
Expand Down
1 change: 1 addition & 0 deletions src/components/transactions/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ErrorMessage = memo(({ title, body, actions, ...rest }: ErrorMessag
border="4px solid #FCEEED"
spacing="extra-loose"
color={color('feedback-error')}
bg={color('bg')}
{...rest}
>
<Stack spacing="base-loose">
Expand Down
79 changes: 73 additions & 6 deletions src/components/transactions/transaction-errors.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { memo } from 'react';
import { useCurrentAccount } from '@common/hooks/account/use-current-account';
import { color, Stack, useClipboard } from '@stacks/ui';
import { color, Flex, Stack, useClipboard, Fade } from '@stacks/ui';
import { useTransactionRequest } from '@common/hooks/transaction/use-transaction';
import { useFetchBalances } from '@common/hooks/account/use-account-info';
import { Caption } from '@components/typography';
Expand All @@ -13,6 +13,7 @@ import { ErrorMessage } from '@components/transactions/error';
import { useDrawers } from '@common/hooks/use-drawers';
import { useRecoilValue } from 'recoil';
import { transactionBroadcastErrorState } from '@store/transactions';
import { useScrollLock } from '@common/hooks/use-scroll-lock';

export const FeeInsufficientFundsErrorMessage = memo(props => {
const currentAccount = useCurrentAccount();
Expand Down Expand Up @@ -99,12 +100,78 @@ export const NoContractErrorMessage = memo(props => {
});

export const UnauthorizedErrorMessage = memo(props => {
useScrollLock(true);
return (
<ErrorMessage
title="Unauthorized request"
body="The transaction request was not properly authorized by any of your accounts. If you've logged in to this app before, then you might need to re-authenticate into this application before attempting to sign a transaction with the Stacks Wallet."
{...props}
/>
<Fade in>
{styles => (
<Flex
position="absolute"
width="100%"
height="100vh"
zIndex={99}
left={0}
top={0}
alignItems="center"
justifyContent="center"
p="loose"
bg="rgba(0,0,0,0.35)"
backdropFilter="blur(10px)"
style={styles}
>
<ErrorMessage
title="Unauthorized request"
body="The transaction request was not properly authorized by any of your accounts. If you've logged in to this app before, then you might need to re-authenticate into this application before attempting to sign a transaction with the Stacks Wallet."
border={'1px solid'}
borderColor={color('border')}
boxShadow="high"
css={{
'& > *': {
pointerEvents: 'all',
},
}}
{...props}
/>
</Flex>
)}
</Fade>
);
});

export const ExpiredRequestErrorMessage = memo(props => {
useScrollLock(true);
return (
<Fade in>
{styles => (
<Flex
position="fixed"
width="100%"
height="100vh"
zIndex={99}
left={0}
top={0}
alignItems="center"
justifyContent="center"
p="loose"
bg="rgba(0,0,0,0.35)"
backdropFilter="blur(10px)"
style={styles}
>
<ErrorMessage
title="Expired request"
body="This transaction request has expired or cannot be validated, please try to re-initiate this transaction request from the original app."
border={'1px solid'}
borderColor={color('border')}
boxShadow="high"
css={{
'& > *': {
pointerEvents: 'all',
},
}}
{...props}
/>
</Flex>
)}
</Fade>
);
});

Expand Down
4 changes: 4 additions & 0 deletions src/pages/transaction/transaction-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { memo } from 'react';
import { useTransactionError } from '@common/hooks/transaction/use-transaction-error';
import {
BroadcastErrorMessage,
ExpiredRequestErrorMessage,
FeeInsufficientFundsErrorMessage,
NoContractErrorMessage,
StxTransferInsufficientFundsErrorMessage,
Expand All @@ -15,6 +16,7 @@ export enum TransactionErrorReason {
BroadcastError = 4,
Unauthorized = 5,
NoContract = 6,
ExpiredRequest = 7,
}

export const TransactionError = memo(() => {
Expand All @@ -31,6 +33,8 @@ export const TransactionError = memo(() => {
return <FeeInsufficientFundsErrorMessage />;
case TransactionErrorReason.Unauthorized:
return <UnauthorizedErrorMessage />;
case TransactionErrorReason.ExpiredRequest:
return <ExpiredRequestErrorMessage />;
default:
return null;
}
Expand Down
45 changes: 44 additions & 1 deletion src/store/transactions/requests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { atom, selector } from 'recoil';
import { atom, selector, waitForAll } from 'recoil';
import { getPayloadFromToken } from '@store/transactions/utils';
import { walletState } from '@store/wallet';
import { verifyTxRequest } from '@common/transactions/requests';
import { getRequestOrigin, StorageKey } from '@common/storage';

enum KEYS {
REQUEST_TOKEN = 'requests/REQUEST_TOKEN',
REQUEST_TOKEN_ORIGIN = 'requests/REQUEST_TOKEN_ORIGIN',
REQUEST_TOKEN_VALIDATION = 'requests/REQUEST_TOKEN_VALIDATION',
REQUEST_TOKEN_PAYLOAD = 'requests/REQUEST_TOKEN_PAYLOAD',
ADDRESS = 'requests/ADDRESS',
NETWORK = 'requests/NETWORK',
Expand Down Expand Up @@ -35,6 +40,44 @@ export const requestTokenPayloadState = selector({
},
});

export const requestTokenOriginState = selector({
key: KEYS.REQUEST_TOKEN_ORIGIN,
get: ({ get }) => {
const token = get(requestTokenState);
if (!token) return;
try {
return getRequestOrigin(StorageKey.transactionRequests, token);
} catch (e) {
console.error(e);
return false;
}
},
});

export const transactionRequestValidationState = selector({
key: KEYS.REQUEST_TOKEN_VALIDATION,
get: async ({ get }) => {
const { requestToken, wallet, origin } = get(
waitForAll({
requestToken: requestTokenState,
wallet: walletState,
origin: requestTokenOriginState,
})
);
if (!origin || !wallet || !requestToken) return;
try {
const valid = await verifyTxRequest({
requestToken,
wallet,
appDomain: origin,
});
return !!valid;
} catch (e) {
return false;
}
},
});

export const transactionRequestStxAddressState = selector({
key: KEYS.ADDRESS,
get: ({ get }) => get(requestTokenPayloadState)?.stxAddress,
Expand Down

0 comments on commit 6a6640c

Please sign in to comment.