Skip to content

Commit

Permalink
Fix sign message functionality using web wallet provider (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
CiprianDraghici committed Nov 28, 2023
1 parent 868538a commit cdde509
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 84 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@fortawesome/free-solid-svg-icons": "6.1.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@multiversx/sdk-core": "12.14.0",
"@multiversx/sdk-dapp": "2.23.0",
"@multiversx/sdk-dapp": "2.24.0",
"@multiversx/sdk-dapp-sc-explorer": "0.0.1-beta.1",
"@multiversx/sdk-native-auth-server": "1.0.10",
"@multiversx/sdk-wallet": "4.2.0",
Expand Down
36 changes: 18 additions & 18 deletions src/components/Template/components/Environment/Environment.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useCallback } from 'react';
import Select, { components, SingleValue } from 'react-select';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import { useLocation, useNavigate } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faArrowDownLong,
faArrowUpLong
faArrowUpLong,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import classNames from 'classnames';
import type { OptionType } from './types';
import styles from './styles.module.scss';
import { ActionTypeEnum } from 'context/reducer';
import { useLocation, useNavigate } from 'react-router-dom';
import Select, { components, SingleValue } from 'react-select';
import { useDispatch } from 'context';
import { NETWORK } from 'localConstants/environment';
import { ActionTypeEnum } from 'context/reducer';
import { useChain } from 'hooks/useChain';
import { NETWORK_KEY } from 'localConstants/environment';
import styles from './styles.module.scss';
import type { OptionType } from './types';

const customComponents = {
Control: (props: any) => (
Expand All @@ -39,11 +39,11 @@ const customComponents = {
<components.Option
{...props}
className={classNames(styles.option, {
[styles.selected]: props.isSelected
[styles.selected]: props.isSelected,
})}
/>
),
IndicatorSeparator: null
IndicatorSeparator: null,
};

export const Environment = () => {
Expand All @@ -56,31 +56,31 @@ export const Environment = () => {
const options: OptionType[] = Object.values(EnvironmentsEnum).map(
(chain) => ({
label: chain,
value: chain
})
value: chain,
}),
);

const onChange = useCallback(
async (option: SingleValue<OptionType>) => {
if (option) {
dispatch({
type: ActionTypeEnum.switchDappEnvironment,
dappEnvironment: option.value as EnvironmentsEnum
dappEnvironment: option.value as EnvironmentsEnum,
});

const params = new URLSearchParams(window.location.search);
params.set(NETWORK, option.value);
params.set(NETWORK_KEY, option.value);

navigate(
{
pathname,
search: params.toString()
search: params.toString(),
},
{ replace: true }
{ replace: true },
);
}
},
[pathname, navigate, dispatch]
[pathname, navigate, dispatch],
);

return (
Expand Down
19 changes: 10 additions & 9 deletions src/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {
createContext,
useReducer,
useContext,
PropsWithChildren
PropsWithChildren,
} from 'react';

import { DispatchType, reducer } from './reducer';
import { StateType, initializer } from './state';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import { DappProvider } from '@multiversx/sdk-dapp/wrappers';
import { useLocation } from 'react-router-dom';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import { NETWORK_KEY } from 'localConstants';
import { DispatchType, reducer } from './reducer';
import { StateType, initializer } from './state';

const Context = createContext<StateType | undefined>(undefined);
const Dispatch = createContext<DispatchType | undefined>(undefined);
Expand All @@ -19,11 +20,11 @@ const ContextProvider = (props: PropsWithChildren) => {

const { search } = useLocation();
const params = new URLSearchParams(search);
const network = params.get('network') ?? initializer.dappEnvironment;
const network = params.get(NETWORK_KEY) ?? initializer.dappEnvironment;

const [state, dispatch] = useReducer(reducer, {
...initializer,
dappEnvironment: network as EnvironmentsEnum
dappEnvironment: network as EnvironmentsEnum,
});

return (
Expand All @@ -33,7 +34,7 @@ const ContextProvider = (props: PropsWithChildren) => {
customNetworkConfig={{
name: 'customConfig',
apiTimeout: 6000,
walletConnectV2ProjectId: '9b1a9564f91cb659ffe21b73d5c4e2d8'
walletConnectV2ProjectId: '9b1a9564f91cb659ffe21b73d5c4e2d8',
}}
>
<Dispatch.Provider value={dispatch}>{children}</Dispatch.Provider>
Expand All @@ -47,7 +48,7 @@ const useGlobalContext = () => {

if (context === undefined) {
throw new Error(
'The useGlobalContext hook must be used within a Context.Provider.'
'The useGlobalContext hook must be used within a Context.Provider.',
);
} else {
return context;
Expand All @@ -59,7 +60,7 @@ const useDispatch = () => {

if (context === undefined) {
throw new Error(
'The useDispatch hook must be used within a Dispatch.Provider.'
'The useDispatch hook must be used within a Dispatch.Provider.',
);
} else {
return context;
Expand Down
12 changes: 6 additions & 6 deletions src/hooks/useChain.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useLocation } from 'react-router-dom';
import { useGetNetworkConfig } from '@multiversx/sdk-dapp/hooks';
import { NETWORK } from '../localConstants';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import { useMemo } from 'react';
import { getEnvironmentForChainId } from '@multiversx/sdk-dapp/apiCalls/configuration/getEnvironmentForChainId';
import { useGetNetworkConfig } from '@multiversx/sdk-dapp/hooks';
import { EnvironmentsEnum } from '@multiversx/sdk-dapp/types';
import { useLocation } from 'react-router-dom';
import { NETWORK_KEY } from 'localConstants';

export const useChain = () => {
const { search } = useLocation();
const { network } = useGetNetworkConfig();

const entries = Object.fromEntries(new URLSearchParams(search));
const environment = entries[NETWORK] as EnvironmentsEnum;
const environment = entries[NETWORK_KEY] as EnvironmentsEnum;
const chain = useMemo<EnvironmentsEnum>(
() => environment || getEnvironmentForChainId(network.chainId),
[environment, network.chainId]
[environment, network.chainId],
);

return { chain };
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/useExecuteOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useCallback, useRef } from "react";

export const useExecuteOnce = () => {
const inProgress = useRef(false);

return useCallback(
(callback: CallableFunction) => {
if (inProgress.current) {
return;
}

inProgress.current = true;
callback();
},
[],
);
};
2 changes: 1 addition & 1 deletion src/localConstants/environment.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const NETWORK = 'network';
export const NETWORK_KEY = 'network';
1 change: 1 addition & 0 deletions src/localConstants/storage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const MESSAGE_TO_SIGN_KEY = 'messageToSign';
export const MESSAGE_KEY = 'message';
export const SIGNATURE_KEY = 'signature';
export const STATUS_KEY = 'status';

export const DEPLOY_SESSION_ID = 'deploySessionId';
export const UPGRADE_SESSION_ID = 'upgradeSessionId';
Expand Down
7 changes: 1 addition & 6 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ import { ChangeEvent, useEffect } from 'react';
import { CopyButton } from '@multiversx/sdk-dapp/UI/CopyButton';
import classNames from 'classnames';
import { ErrorMessage, Field, Form, useFormikContext } from 'formik';
import { useSearchParams } from 'react-router-dom';
import { useExecuteOnce } from 'hooks/useExecuteOnce';
import { useSignMessageSectionContext } from 'pages/SignMessage/context';
import styles from 'pages/SignMessage/styles.module.scss';
import { useSignMessageForm } from '../hooks/useSignMessageForm';
import { SignMessageFormValues } from '../types';

export const SignMessageForm = () => {
const { setFieldValue } = useFormikContext<SignMessageFormValues>();
const { messageToSign, setMessageToSign } = useSignMessageSectionContext();
const { signedMessagePayload, handleClear } = useSignMessageForm();
const { messageToSign, setMessageToSign, signedMessagePayload } =
useSignMessageSectionContext();
const { handleClear } = useSignMessageForm();

const [searchParams] = useSearchParams();
const executeOnce = useExecuteOnce();
const { buildSignaturePayloadFromWebWalletResponse } = useSignMessageForm();

useEffect(() => {
buildSignaturePayloadFromWebWalletResponse();
}, [buildSignaturePayloadFromWebWalletResponse, searchParams]);
executeOnce(buildSignaturePayloadFromWebWalletResponse);
}, []);

return (
<Form className={styles.sign}>
Expand All @@ -44,7 +45,9 @@ export const SignMessageForm = () => {
<strong>Signature payload:</strong>

<div className={styles.code}>
<pre data-testid='signaturePayload' className={styles.value}>{signedMessagePayload}</pre>
<pre data-testid='signaturePayload' className={styles.value}>
{signedMessagePayload}
</pre>
<CopyButton text={signedMessagePayload} className={styles.copy} />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import { routeNames } from 'routes';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks';
import { SignedMessageStatusesEnum } from 'pages/SignMessage/types';
import { Address, SignableMessage } from '@multiversx/sdk-core';
import { useFormikContext } from 'formik';
import { SignMessageFormValues } from '../types';
import { useSignMessageSectionContext } from 'pages/SignMessage/context';
import { useCallback } from 'react';
import { Address, SignableMessage } from '@multiversx/sdk-core';
import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks';
import { useGetLoginInfo } from '@multiversx/sdk-dapp/hooks/account/useGetLoginInfo';
import { LoginMethodsEnum } from '@multiversx/sdk-dapp/types';
import { MESSAGE_KEY, SIGNATURE_KEY } from 'localConstants/storage';
import { useFormikContext } from 'formik';
import { useLocation, useSearchParams } from 'react-router-dom';
import { MESSAGE_KEY, SIGNATURE_KEY, STATUS_KEY } from 'localConstants/storage';
import { useSignMessageSectionContext } from 'pages/SignMessage/context';
import { SignedMessageStatusesEnum } from 'pages/SignMessage/types';
import { SignMessageFormValues } from '../types';

export const useSignMessageForm = () => {
const { setFieldValue } = useFormikContext<SignMessageFormValues>();
const {
signedMessagePayload,
setSignedMessagePayload,
setMessageToSign,
persistedMessageToSign
} = useSignMessageSectionContext();
const { setSignedMessagePayload, setMessageToSign, persistedMessageToSign } =
useSignMessageSectionContext();

const [searchParams] = useSearchParams();
const { address } = useGetAccountInfo();
const { loginMethod } = useGetLoginInfo();
const navigate = useNavigate();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams();
const { search } = useLocation();
const entries = Object.fromEntries(new URLSearchParams(search));

// This is the case when the user is not logged in, insert a message to sign and press the sign button
// The user will be redirected to the unlock page, having the message to sign in the url (if is not web wallet) or in the session storage (if is web wallet)
const buildSignaturePayloadFromWebWalletResponse = useCallback(() => {
const signatureParam = searchParams.get(SIGNATURE_KEY) ?? '';
const signStatusParam = searchParams.get('status');
const buildSignaturePayloadFromWebWalletResponse = () => {
const signatureParam = entries[SIGNATURE_KEY];
const signStatusParam = entries[STATUS_KEY];

const isMessageSigned =
signStatusParam === SignedMessageStatusesEnum.signed;
Expand All @@ -40,7 +38,11 @@ export const useSignMessageForm = () => {
setMessageToSign(persistedMessageToSign);
} else {
// If the user is not using the web wallet, we need to get the message from the url
const searchParamsMessage = searchParams.get(MESSAGE_KEY) ?? '';
const searchParamsMessage = entries[MESSAGE_KEY];
if (!searchParamsMessage) {
return;
}

setFieldValue('message', searchParamsMessage);
setMessageToSign(searchParamsMessage);
}
Expand All @@ -49,27 +51,30 @@ export const useSignMessageForm = () => {
if (signatureParam && isMessageSigned) {
const signedPayload = new SignableMessage({
...(address ? { address: new Address(address) } : {}),
message: Buffer.from(persistedMessageToSign)
message: Buffer.from(persistedMessageToSign),
});

const messageObj = JSON.parse(JSON.stringify(signedPayload));
messageObj.signature = `0x${signatureParam}`;

setSignedMessagePayload(JSON.stringify(messageObj, null, 2));
navigate(routeNames.signMessage, { replace: true });
}
}, []);
};

const handleClear = useCallback(() => {
setSignedMessagePayload('');
setMessageToSign('');
setFieldValue('message', '');
}, [setFieldValue, setMessageToSign, setSignedMessagePayload]);
setSearchParams({}, { replace: true });
}, [
setFieldValue,
setMessageToSign,
setSearchParams,
setSignedMessagePayload,
]);

return {
buildSignaturePayloadFromWebWalletResponse,
handleClear,
signedMessagePayload,
setSignedMessagePayload
};
};
Loading

0 comments on commit cdde509

Please sign in to comment.