From cdde5098e4fb78783319cd34df73d047dc1aaead Mon Sep 17 00:00:00 2001 From: Ciprian Draghici Date: Tue, 28 Nov 2023 14:45:03 +0200 Subject: [PATCH] Fix sign message functionality using web wallet provider (#75) --- package.json | 2 +- .../components/Environment/Environment.tsx | 36 +++++------ src/context/index.tsx | 19 +++--- src/hooks/useChain.ts | 12 ++-- src/hooks/useExecuteOnce.ts | 17 ++++++ src/localConstants/environment.ts | 2 +- src/localConstants/storage.ts | 1 + src/main.tsx | 7 +-- .../components/SignMessageForm.tsx | 17 +++--- .../hooks/useSignMessageForm.ts | 59 ++++++++++--------- .../hooks/useSignMessageSectionActions.ts | 23 +++++--- yarn.lock | 40 +++++++++++++ 12 files changed, 151 insertions(+), 84 deletions(-) create mode 100644 src/hooks/useExecuteOnce.ts diff --git a/package.json b/package.json index c02e8d3..d0bc36a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Template/components/Environment/Environment.tsx b/src/components/Template/components/Environment/Environment.tsx index 3269d3b..7d0ea2b 100644 --- a/src/components/Template/components/Environment/Environment.tsx +++ b/src/components/Template/components/Environment/Environment.tsx @@ -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) => ( @@ -39,11 +39,11 @@ const customComponents = { ), - IndicatorSeparator: null + IndicatorSeparator: null, }; export const Environment = () => { @@ -56,8 +56,8 @@ export const Environment = () => { const options: OptionType[] = Object.values(EnvironmentsEnum).map( (chain) => ({ label: chain, - value: chain - }) + value: chain, + }), ); const onChange = useCallback( @@ -65,22 +65,22 @@ export const Environment = () => { 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 ( diff --git a/src/context/index.tsx b/src/context/index.tsx index b51fb81..2e79a08 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -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(undefined); const Dispatch = createContext(undefined); @@ -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 ( @@ -33,7 +34,7 @@ const ContextProvider = (props: PropsWithChildren) => { customNetworkConfig={{ name: 'customConfig', apiTimeout: 6000, - walletConnectV2ProjectId: '9b1a9564f91cb659ffe21b73d5c4e2d8' + walletConnectV2ProjectId: '9b1a9564f91cb659ffe21b73d5c4e2d8', }} > {children} @@ -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; @@ -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; diff --git a/src/hooks/useChain.ts b/src/hooks/useChain.ts index 8a2483a..47bcde0 100644 --- a/src/hooks/useChain.ts +++ b/src/hooks/useChain.ts @@ -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( () => environment || getEnvironmentForChainId(network.chainId), - [environment, network.chainId] + [environment, network.chainId], ); return { chain }; diff --git a/src/hooks/useExecuteOnce.ts b/src/hooks/useExecuteOnce.ts new file mode 100644 index 0000000..0170221 --- /dev/null +++ b/src/hooks/useExecuteOnce.ts @@ -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(); + }, + [], + ); +}; diff --git a/src/localConstants/environment.ts b/src/localConstants/environment.ts index e1f561c..ad084c4 100644 --- a/src/localConstants/environment.ts +++ b/src/localConstants/environment.ts @@ -1 +1 @@ -export const NETWORK = 'network'; +export const NETWORK_KEY = 'network'; diff --git a/src/localConstants/storage.ts b/src/localConstants/storage.ts index d4e787c..0032547 100644 --- a/src/localConstants/storage.ts +++ b/src/localConstants/storage.ts @@ -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'; diff --git a/src/main.tsx b/src/main.tsx index b2bc486..ea68d00 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - - - , -); +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/src/pages/SignMessage/components/SignMessageSection/components/SignMessageForm.tsx b/src/pages/SignMessage/components/SignMessageSection/components/SignMessageForm.tsx index 02ceea9..6064e9c 100644 --- a/src/pages/SignMessage/components/SignMessageSection/components/SignMessageForm.tsx +++ b/src/pages/SignMessage/components/SignMessageSection/components/SignMessageForm.tsx @@ -2,7 +2,7 @@ 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'; @@ -10,15 +10,16 @@ import { SignMessageFormValues } from '../types'; export const SignMessageForm = () => { const { setFieldValue } = useFormikContext(); - 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 (
@@ -44,7 +45,9 @@ export const SignMessageForm = () => { Signature payload:
-
{signedMessagePayload}
+
+              {signedMessagePayload}
+            
diff --git a/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageForm.ts b/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageForm.ts index 7148c47..c10924c 100644 --- a/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageForm.ts +++ b/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageForm.ts @@ -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(); - 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; @@ -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); } @@ -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 }; }; diff --git a/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageSectionActions.ts b/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageSectionActions.ts index f701556..d59be2c 100644 --- a/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageSectionActions.ts +++ b/src/pages/SignMessage/components/SignMessageSection/hooks/useSignMessageSectionActions.ts @@ -1,13 +1,13 @@ import { useCallback } from 'react'; -import { routeNames } from 'routes'; +import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/hooks'; +import { useGetLoginInfo } from '@multiversx/sdk-dapp/hooks/account/useGetLoginInfo'; import { LoginMethodsEnum } from '@multiversx/sdk-dapp/types'; import { signMessage } from '@multiversx/sdk-dapp/utils'; -import { MESSAGE_KEY } from 'localConstants/storage'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useGetLoginInfo } from '@multiversx/sdk-dapp/hooks/account/useGetLoginInfo'; -import { useGetIsLoggedIn } from '@multiversx/sdk-dapp/hooks'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useCallbackRoute } from 'hooks/useCallbackRoute'; +import { MESSAGE_KEY, SIGNATURE_KEY, STATUS_KEY } from 'localConstants/storage'; import { useSignMessageSectionContext } from 'pages/SignMessage/context'; +import { routeNames } from 'routes'; export const useSignMessageSectionActions = () => { const { setSignedMessagePayload, messageToSign } = @@ -19,7 +19,12 @@ export const useSignMessageSectionActions = () => { const navigate = useNavigate(); const callbackRoute = useCallbackRoute(); + const [searchParams] = useSearchParams(); + const handleSignMessage = useCallback(async () => { + searchParams.delete(SIGNATURE_KEY); + searchParams.delete(STATUS_KEY); + if (!isLoggedIn) { const route = search ? `${routeNames.unlock}${search}&callbackUrl=${callbackRoute}` @@ -29,14 +34,14 @@ export const useSignMessageSectionActions = () => { navigate( isWallet ? encodeURIComponent(route) - : `${route}?${MESSAGE_KEY}=${messageToSign}` + : `${route}?${MESSAGE_KEY}=${messageToSign}`, ); return; } const signableMessage = await signMessage({ message: messageToSign, - callbackRoute: routeNames.signMessage + callbackRoute: `${routeNames.signMessage}${search}`, }); if (!signableMessage) { @@ -51,10 +56,10 @@ export const useSignMessageSectionActions = () => { messageToSign, navigate, search, - setSignedMessagePayload + setSignedMessagePayload, ]); return { - handleSignMessage + handleSignMessage, }; }; diff --git a/yarn.lock b/yarn.lock index 2c51cc6..1d267db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1891,6 +1891,46 @@ qrcode "1.5.0" swr "2.2.0" +"@multiversx/sdk-dapp@2.24.0": + version "2.24.0" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-dapp/-/sdk-dapp-2.24.0.tgz#d78fd6ec52789a4b3113f87f739624396e5b956d" + integrity sha512-+SnHcKBWkUhzynigrkUeHYZCQUtEzb5k0lP2kXMn7xNmfHmVfryBDTf52zCACj6M+CJSFYkwrCcjNBJgblvqmw== + dependencies: + "@multiversx/sdk-core" "12.6.0" + "@multiversx/sdk-extension-provider" "3.0.0" + "@multiversx/sdk-hw-provider" "6.4.0" + "@multiversx/sdk-native-auth-client" "1.0.5" + "@multiversx/sdk-network-providers" "1.5.0" + "@multiversx/sdk-opera-provider" "1.0.0-alpha.1" + "@multiversx/sdk-wallet" "4.2.0" + "@multiversx/sdk-wallet-connect-provider" "4.0.4" + "@multiversx/sdk-web-wallet-provider" "3.1.0" + "@reduxjs/toolkit" "1.8.2" + axios "0.24.0" + bignumber.js "9.x" + linkify-react "4.0.2" + linkifyjs "4.0.2" + lodash.debounce "4.0.8" + lodash.isequal "4.5.0" + lodash.omit "4.5.0" + lodash.throttle "4.1.1" + lodash.uniqby "4.7.0" + qs "6.10.3" + react-idle-timer "5.0.0" + react-redux "8.0.2" + redux-persist "6.0.0" + reselect "4.0.0" + shx "0.3.4" + socket.io-client "4.6.1" + optionalDependencies: + "@fortawesome/fontawesome-svg-core" "6.4.0" + "@fortawesome/free-solid-svg-icons" "6.4.0" + "@fortawesome/react-fontawesome" "0.2.0" + classnames "2.3.1" + platform "1.3.6" + qrcode "1.5.0" + swr "2.2.0" + "@multiversx/sdk-extension-provider@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@multiversx/sdk-extension-provider/-/sdk-extension-provider-3.0.0.tgz#e0e178ee5555f9440457547759621f5c3152c5fa"