From b3f3f9c55180be79fe42bf185c5607417e054dc7 Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 21 Sep 2023 17:24:05 +0200 Subject: [PATCH 01/52] feat: Implement social login with MPC Core SDK --- .github/workflows/build/action.yml | 3 + .github/workflows/deploy.yml | 4 +- package.json | 2 + public/serviceworker/sw.js | 318 +++++++ .../common/ConnectWallet/AccountCenter.tsx | 3 + .../common/ConnectWallet/MPCWallet.tsx | 33 + .../ConnectWallet/MPCWalletProvider.tsx | 38 + .../common/ConnectWallet/WalletDetails.tsx | 3 + .../create/steps/ConnectWalletStep/index.tsx | 9 +- src/config/constants.ts | 6 + src/hooks/wallets/consts.ts | 2 + src/hooks/wallets/mpc/useMPC.ts | 82 ++ src/hooks/wallets/mpc/useMPCWallet.ts | 97 ++ src/hooks/wallets/wallets.ts | 6 +- src/pages/_app.tsx | 7 +- src/services/mpc/icon.ts | 12 + src/services/mpc/module.ts | 94 ++ yarn.lock | 893 +++++++++++++++++- 18 files changed, 1594 insertions(+), 18 deletions(-) create mode 100644 public/serviceworker/sw.js create mode 100644 src/components/common/ConnectWallet/MPCWallet.tsx create mode 100644 src/components/common/ConnectWallet/MPCWalletProvider.tsx create mode 100644 src/hooks/wallets/mpc/useMPC.ts create mode 100644 src/hooks/wallets/mpc/useMPCWallet.ts create mode 100644 src/services/mpc/icon.ts create mode 100644 src/services/mpc/module.ts diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 0cfb040d50..5f270b17f3 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,3 +43,6 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} + NEXT_PUBLIC_WEB3AUTH_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_CLIENT_ID }} + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_CLIENT_ID }} + NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a05f029ff..f33c09fb11 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -67,9 +67,9 @@ jobs: # Extract branch name - name: Extract branch name shell: bash - ## Cut off "refs/heads/" and only allow alphanumeric characters, + ## Cut off "refs/heads/", only allow alphanumeric characters and convert to lower case, ## e.g. "refs/heads/features/hello-1.2.0" -> "features_hello_1_2_0" - run: echo "branch=$(echo $GITHUB_HEAD_REF | sed 's/refs\/heads\///' | sed 's/[^a-z0-9]/_/ig')" >> $GITHUB_OUTPUT + run: echo "branch=$(echo $GITHUB_HEAD_REF | sed 's/refs\/heads\///' | sed 's/[^a-z0-9]/_/ig' | sed 's/[A-Z]/\L&/g')" >> $GITHUB_OUTPUT id: extract_branch # Deploy to S3 diff --git a/package.json b/package.json index 97bf64ec71..0e4a65836e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,8 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.5", + "@web3auth/mpc-core-kit": "^1.0.1", + "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.29.2", "ethereum-blockies-base64": "^1.0.2", diff --git a/public/serviceworker/sw.js b/public/serviceworker/sw.js new file mode 100644 index 0000000000..ad32a6ed6d --- /dev/null +++ b/public/serviceworker/sw.js @@ -0,0 +1,318 @@ +/* eslint-disable */ +function getScope() { + return self.registration.scope +} + +self.addEventListener('message', function (event) { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) + +self.addEventListener('fetch', function (event) { + try { + const url = new URL(event.request.url) + const redirectURL = self.location.url + if (url.pathname.includes('redirect') && url.href.includes(getScope())) { + event.respondWith( + new Response( + new Blob( + [ + ` + + + + + + + Redirect + + + + +
+
+
+
+
+
+

You can close this window now

+
+ + + + + + ${''} + `, + ], + { type: 'text/html' }, + ), + ), + ) + } + } catch (error) { + console.error(error) + } +}) diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 03a0aab974..082a0fab54 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -13,6 +13,7 @@ import ChainSwitcher from '../ChainSwitcher' import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' +import { MPCWallet } from './MPCWallet' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) @@ -114,6 +115,8 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { + + diff --git a/src/components/common/ConnectWallet/MPCWallet.tsx b/src/components/common/ConnectWallet/MPCWallet.tsx new file mode 100644 index 0000000000..3799144921 --- /dev/null +++ b/src/components/common/ConnectWallet/MPCWallet.tsx @@ -0,0 +1,33 @@ +import { Box, Button, CircularProgress } from '@mui/material' +import { useContext } from 'react' +import { MpcWalletContext } from './MPCWalletProvider' + +export const MPCWallet = () => { + const { loginPending, triggerLogin, resetAccount, userInfo } = useContext(MpcWalletContext) + + return ( + <> + {userInfo.email ? ( + <> + + + + + + + ) : ( + + )} + + ) +} diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx new file mode 100644 index 0000000000..30e2be7117 --- /dev/null +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -0,0 +1,38 @@ +import { useMPCWallet, MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' +import { createContext, type ReactElement } from 'react' + +type MPCWalletContext = { + loginPending: boolean + triggerLogin: () => Promise + resetAccount: () => Promise + upsertPasswordBackup: (password: string) => Promise + recoverFactorWithPassword: (password: string) => Promise + walletState: MPCWalletState + userInfo: { + email: string | undefined + } +} + +export const MpcWalletContext = createContext({ + loginPending: false, + walletState: MPCWalletState.NOT_INITIALIZED, + triggerLogin: () => Promise.resolve(), + resetAccount: () => Promise.resolve(), + upsertPasswordBackup: () => Promise.resolve(), + recoverFactorWithPassword: () => Promise.resolve(), + userInfo: { + email: undefined, + }, +}) + +export const MpcWalletProvider = ({ children }: { children: ReactElement }) => { + const mpcValue = useMPCWallet() + + return ( + + {children} + + ) +} diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index e91f11973e..c4fb5f8c2d 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -3,6 +3,7 @@ import type { ReactElement } from 'react' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import { MPCWallet } from './MPCWallet' const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement => { const connectWallet = useConnectWallet() @@ -21,6 +22,8 @@ const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement + + ) } diff --git a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx index a955d2cb4d..8081ca8e8a 100644 --- a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx +++ b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Box, Button, Grid, Typography } from '@mui/material' +import { Box, Button, Divider, Grid, Typography } from '@mui/material' import useWallet from '@/hooks/wallets/useWallet' import { useCurrentChain } from '@/hooks/useChains' import { isPairingSupported } from '@/services/pairing/utils' @@ -13,6 +13,7 @@ import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import PairingDescription from '@/components/common/PairingDetails/PairingDescription' import PairingQRCode from '@/components/common/PairingDetails/PairingQRCode' import { usePendingSafe } from '../StatusStep/usePendingSafe' +import { MPCWallet } from '@/components/common/ConnectWallet/MPCWallet' const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps) => { const [pendingSafe] = usePendingSafe() @@ -45,6 +46,12 @@ const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps Connect + + + or + + + {isSupported && ( diff --git a/src/config/constants.ts b/src/config/constants.ts index 577265e35f..6de429bbe9 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -97,3 +97,9 @@ export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST || fals export const REDEFINE_SIMULATION_URL = 'https://dashboard.redefine.net/reports/' export const REDEFINE_API = process.env.NEXT_PUBLIC_REDEFINE_API export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FIzCYmVMGrrV8Nhg4THdwI' + +// Social Login +export const WEB3_AUTH_CLIENT_ID = process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID || '' +export const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '' +export const WEB3AUTH_VERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID || '' +export const AUTHENTICATOR_APP_VERIFIER = process.env.NEXT_PUBLIC_AUTHENTICATOR_APP_VERIFIER || '' diff --git a/src/hooks/wallets/consts.ts b/src/hooks/wallets/consts.ts index 10a3bb99fa..c6da493a07 100644 --- a/src/hooks/wallets/consts.ts +++ b/src/hooks/wallets/consts.ts @@ -1,6 +1,7 @@ export const enum WALLET_KEYS { INJECTED = 'INJECTED', WALLETCONNECT_V2 = 'WALLETCONNECT_V2', + SOCIAL = 'SOCIAL_LOGIN', COINBASE = 'COINBASE', PAIRING = 'PAIRING', LEDGER = 'LEDGER', @@ -13,6 +14,7 @@ export const CGW_NAMES: { [key in WALLET_KEYS]: string | undefined } = { [WALLET_KEYS.WALLETCONNECT_V2]: 'walletConnect_v2', [WALLET_KEYS.COINBASE]: 'coinbase', [WALLET_KEYS.PAIRING]: 'safeMobile', + [WALLET_KEYS.SOCIAL]: 'socialLogin', [WALLET_KEYS.LEDGER]: 'ledger', [WALLET_KEYS.TREZOR]: 'trezor', [WALLET_KEYS.KEYSTONE]: 'keystone', diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts new file mode 100644 index 0000000000..cf1472ab13 --- /dev/null +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -0,0 +1,82 @@ +import { useEffect } from 'react' +import ExternalStore from '@/services/ExternalStore' +import { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK, COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import { CHAIN_NAMESPACES } from '@web3auth/base' + +import { WEB3_AUTH_CLIENT_ID } from '@/config/constants' +import { useCurrentChain } from '@/hooks/useChains' +import { getRpcServiceUrl } from '../web3' +import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' + +const { getStore, setStore, useStore } = new ExternalStore() + +export const useInitMPC = () => { + const chain = useCurrentChain() + const onboard = useOnboard() + + useEffect(() => { + if (!chain || !onboard) { + return + } + const chainConfig = { + chainId: `0x${Number(chain.chainId).toString(16)}`, + chainNamespace: CHAIN_NAMESPACES.EIP155, + rpcTarget: getRpcServiceUrl(chain.rpcUri), + displayName: chain.chainName, + blockExplorer: chain.blockExplorerUriTemplate.address, + ticker: chain.nativeCurrency.symbol, + tickerName: chain.nativeCurrency.name, + } + + const web3AuthCoreKit = new Web3AuthMPCCoreKit({ + web3AuthClientId: WEB3_AUTH_CLIENT_ID, + // Available networks are "sapphire_devnet", "sapphire_mainnet" + web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, + baseUrl: `${window.location.origin}/serviceworker`, + uxMode: 'popup', + enableLogging: true, + chainConfig, + }) + + web3AuthCoreKit + .init() + .then(() => { + setStore(web3AuthCoreKit) + + // If rehydration was successful, connect to onboard + if (web3AuthCoreKit.status === COREKIT_STATUS.INITIALIZED) { + console.log('Logged in', web3AuthCoreKit) + // await mpcCoreKit.enableMFA({}) + const connectedWallet = getConnectedWallet(onboard.state.get().wallets) + if (!connectedWallet) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + } else { + // To propagate the changedChain we disconnect and connect + onboard + .disconnectWallet({ + label: ONBOARD_MPC_MODULE_LABEL, + }) + .then(() => + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }), + ) + } + } + }) + .catch((error) => console.error(error)) + }, [chain, onboard]) +} + +export const getMPCCoreKitInstance = getStore + +export default useStore diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts new file mode 100644 index 0000000000..f0d54044c5 --- /dev/null +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -0,0 +1,97 @@ +import { useState } from 'react' +import useMPC from './useMPC' +import BN from 'bn.js' +import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import useOnboard, { connectWallet } from '../useOnboard' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' + +export enum MPCWalletState { + NOT_INITIALIZED, + AUTHENTICATING, + AUTHENTICATED, + CREATING_SECOND_FACTOR, + RECOVERING_ACCOUNT_PASSWORD, + CREATED_SECOND_FACTOR, + FINALIZING_ACCOUNT, + READY, +} + +export type MPCWalletHook = { + upsertPasswordBackup: (password: string) => Promise + recoverFactorWithPassword: (password: string) => Promise + walletState: MPCWalletState + triggerLogin: () => Promise + resetAccount: () => Promise + userInfo: { + email: string | undefined + } +} + +export const useMPCWallet = (): MPCWalletHook => { + const [walletState, setWalletState] = useState(MPCWalletState.NOT_INITIALIZED) + const mpcCoreKit = useMPC() + const onboard = useOnboard() + + const criticalResetAccount = async (): Promise => { + // This is a critical function that should only be used for testing purposes + // Resetting your account means clearing all the metadata associated with it from the metadata server + // The key details will be deleted from our server and you will not be able to recover your account + if (!mpcCoreKit || !mpcCoreKit.metadataKey) { + throw new Error('coreKitInstance is not set or the user is not logged in') + } + + // In web3auth an account is reset by overwriting the metadata with KEY_NOT_FOUND + await mpcCoreKit.tKey.storageLayer.setMetadata({ + privKey: new BN(mpcCoreKit.metadataKey, 'hex'), + input: { message: 'KEY_NOT_FOUND' }, + }) + } + + const triggerLogin = async () => { + if (!onboard) { + console.error('Onboard not initialized') + return + } + + if (!mpcCoreKit) { + console.error('tKey not initialized yet') + return + } + try { + setWalletState(MPCWalletState.AUTHENTICATING) + await mpcCoreKit.loginWithOauth({ + subVerifierDetails: { + typeOfLogin: 'google', + verifier: WEB3AUTH_VERIFIER_ID, + clientId: GOOGLE_CLIENT_ID, + }, + }) + + if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + } + + setWalletState(MPCWalletState.AUTHENTICATED) + } catch (error) { + setWalletState(MPCWalletState.NOT_INITIALIZED) + console.error(error) + } + } + + return { + triggerLogin, + walletState, + recoverFactorWithPassword: () => Promise.resolve(), + resetAccount: criticalResetAccount, + upsertPasswordBackup: () => Promise.resolve(), + userInfo: { + email: mpcCoreKit?.state.userInfo?.email, + }, + } +} diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index 4a9e27aa38..ced8503037 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -12,6 +12,7 @@ import walletConnect from '@web3-onboard/walletconnect' import pairingModule from '@/services/pairing/module' import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' +import MpcModule from '@/services/mpc/module' const prefersDarkMode = (): boolean => { return window?.matchMedia('(prefers-color-scheme: dark)')?.matches @@ -42,6 +43,7 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain), [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }), [WALLET_KEYS.PAIRING]: () => pairingModule(), + [WALLET_KEYS.SOCIAL]: () => MpcModule(), [WALLET_KEYS.LEDGER]: () => ledgerModule(), [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }), [WALLET_KEYS.KEYSTONE]: () => keystoneModule(), @@ -64,7 +66,9 @@ export const getSupportedWallets = (chain: ChainInfo): WalletInit[] => { if (window.Cypress && CYPRESS_MNEMONIC) { return [e2eWalletModule(chain.rpcUri)] } - const enabledWallets = Object.entries(WALLET_MODULES).filter(([key]) => isWalletSupported(chain.disabledWallets, key)) + const enabledWallets = Object.entries(WALLET_MODULES).filter( + ([key]) => key === WALLET_KEYS.SOCIAL || isWalletSupported(chain.disabledWallets, key), + ) if (enabledWallets.length === 0) { return [WALLET_MODULES.INJECTED(chain)] diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 568fa4f1e5..75c40a83b5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,6 +37,8 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' +import { useInitMPC } from '@/hooks/wallets/mpc/useMPC' +import { MpcWalletProvider } from '@/components/common/ConnectWallet/MPCWalletProvider' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -57,6 +59,7 @@ const InitApp = (): null => { useTxTracking() useSafeMsgTracking() useBeamer() + useInitMPC() return null } @@ -73,7 +76,9 @@ export const AppProviders = ({ children }: { children: ReactNode | ReactNode[] } {(safeTheme: Theme) => ( - {children} + + {children} + )} diff --git a/src/services/mpc/icon.ts b/src/services/mpc/icon.ts new file mode 100644 index 0000000000..c306bf5353 --- /dev/null +++ b/src/services/mpc/icon.ts @@ -0,0 +1,12 @@ +const pairingIcon = ` + + + + + + + + +` + +export default pairingIcon diff --git a/src/services/mpc/module.ts b/src/services/mpc/module.ts new file mode 100644 index 0000000000..48ebc24668 --- /dev/null +++ b/src/services/mpc/module.ts @@ -0,0 +1,94 @@ +import { getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' +import { getWeb3ReadOnly } from '@/hooks/wallets/web3' +import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' +import { type EIP1193Provider } from '@web3-onboard/core' + +const getMPCProvider = () => getMPCCoreKitInstance()?.provider + +const assertDefined = (mpcProvider: T | undefined) => { + if (!mpcProvider) { + throw new Error('MPC provider is not ready. Login and initialize it first') + } + return mpcProvider +} + +export const ONBOARD_MPC_MODULE_LABEL = 'Social Login' + +/** + * Module for MPC wallet created by the Web3Auth tKey MPC. + * We gain access to the provider created by tKey MPC after a successful login. + * This module returns a provider which will always get the current MPC Wallet provider from an ExternalStore and delegate all calls to it. + * + * @returns Custom Onboard MpcModule + */ +function MpcModule(): WalletInit { + return () => { + return { + label: ONBOARD_MPC_MODULE_LABEL, + getIcon: async () => (await import('./icon')).default, + getInterface: async () => { + const provider: EIP1193Provider = { + on: (event, listener) => { + const web3 = assertDefined(getMPCProvider()) + web3.on(event, listener) + }, + request: (request) => { + return new Promise((resolve, reject) => { + try { + console.log('MPC Module RPC Request', request) + const web3 = assertDefined(getMPCProvider()) + const web3ReadOnly = assertDefined(getWeb3ReadOnly()) + /* + * We have to fallback to web3ReadOnly for eth_estimateGas because the provider by Web3Auth does not expose / implement it. + */ + if ('eth_estimateGas' === request.method) { + web3ReadOnly?.send(request.method, request.params ?? []).then(resolve) + return + } + + /* + * If the provider is defined we already have access to the accounts. So we can just reply with the current account. + */ + if ('eth_requestAccounts' === request.method) { + web3.request({ method: 'eth_accounts' }).then(resolve) + return + } + + if ('wallet_switchEthereumChain' === request.method) { + // The MPC provider always uses the current chain as chain. Nothing to do here. + resolve(null) + return + } + + // Default: we call the inner provider + web3.request(request).then(resolve) + return + } catch (error) { + reject( + new ProviderRpcError({ + // TODO: This error code is usually for user rejection. But 4009 for instance has no effect with onboard + code: 4001, + message: 'Provider is unavailable', + }), + ) + } + }) + }, + removeListener: (event, listener) => { + const web3 = assertDefined(getMPCProvider()) + return web3.removeListener(event, listener) + }, + disconnect: () => { + getMPCCoreKitInstance()?.logout() + }, + } + + return { + provider, + } + }, + } + } +} + +export default MpcModule diff --git a/yarn.lock b/yarn.lock index fc3e6dd9ea..f095bc68f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1042,6 +1042,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.21.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -1081,6 +1088,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@chaitanyapotti/register-service-worker@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@chaitanyapotti/register-service-worker/-/register-service-worker-1.7.3.tgz#0e670b8a4de1c319a147700d7ebb4f8a2b2e1b84" + integrity sha512-ywnUSfwvqdHchO3ELFWP6hlnhzc2UUETbk+TaBT/vicuMnJbnBLuDCcoy3aWvUE9bjmzg4QQpssRLSz1iZ7XRA== + "@coinbase/wallet-sdk@^3.7.1": version "3.7.1" resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.7.1.tgz#44b3b7a925ff5cc974e4cbf7a44199ffdcf03541" @@ -1361,11 +1373,24 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.5" -"@ethereumjs/rlp@^4.0.1": +"@ethereumjs/common@^3.0.0", "@ethereumjs/common@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.2.0.tgz#b71df25845caf5456449163012074a55f048e0a0" + integrity sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA== + dependencies: + "@ethereumjs/util" "^8.1.0" + crc-32 "^1.2.0" + +"@ethereumjs/rlp@^4.0.0", "@ethereumjs/rlp@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== +"@ethereumjs/rlp@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.0.tgz#dd81b32b2237bc32fb1b54534f8ff246a6c89d9b" + integrity sha512-WuS1l7GJmB0n0HsXLozCoEFc9IwYgf3l0gCkKVYgR67puVF1O4OpEaN0hWmm1c+iHUHFCKt1hJrvy5toLg+6ag== + "@ethereumjs/tx@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.0.0.tgz#8dfd91ed6e91e63996e37b3ddc340821ebd48c81" @@ -1398,7 +1423,17 @@ "@ethereumjs/common" "^2.6.4" ethereumjs-util "^7.1.5" -"@ethereumjs/util@^8.1.0": +"@ethereumjs/tx@^4.0.0", "@ethereumjs/tx@^4.1.2": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.2.0.tgz#5988ae15daf5a3b3c815493bc6b495e76009e853" + integrity sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw== + dependencies: + "@ethereumjs/common" "^3.2.0" + "@ethereumjs/rlp" "^4.0.1" + "@ethereumjs/util" "^8.1.0" + ethereum-cryptography "^2.0.0" + +"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.0.6", "@ethereumjs/util@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== @@ -1407,6 +1442,14 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" +"@ethereumjs/util@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.0.0.tgz#ac5945c629f3ab2ac584d8b12a8513e8eac29dc4" + integrity sha512-V8062I+ZXfFxtFLp7xsPeiT1IxDaVOZaM78nGj1gsWUFeZ8SgADMLDKWehp+muTy1JRbVoXFljZ1qoyv9ji/2g== + dependencies: + "@ethereumjs/rlp" "^5.0.0" + ethereum-cryptography "^2.1.2" + "@ethersproject/abi@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" @@ -2748,6 +2791,18 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@metamask/eth-sig-util@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz#a47f62800ee1917fef976ba67544a0ccd7d1bd6b" + integrity sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ== + dependencies: + "@ethereumjs/util" "^8.0.6" + bn.js "^4.12.0" + ethereum-cryptography "^2.0.0" + ethjs-util "^0.1.6" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + "@metamask/obs-store@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3" @@ -2756,6 +2811,22 @@ "@metamask/safe-event-emitter" "^2.0.0" through2 "^2.0.3" +"@metamask/rpc-errors@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-5.1.1.tgz#f82732ad0952d34d219eca42699c0c74bee95a9e" + integrity sha512-JjZnDi2y2CfvbohhBl+FOQRzmFlJpybcQlIk37zEX8B96eVSPbH/T8S0p7cSF8IE33IWx6JkD8Ycsd+2TXFxCw== + dependencies: + "@metamask/utils" "^5.0.0" + fast-safe-stringify "^2.0.6" + +"@metamask/rpc-errors@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.0.0.tgz#6cd2499a0f01e027e7b627eceae17141712fa734" + integrity sha512-sAZwcdmidJDPbZV3XSKcWZC7CSTdjqDNRsDDdb2SstCOLEJtNqHpx32FWgwWB0arqWxUcUxYxgR39edUbsWz7A== + dependencies: + "@metamask/utils" "^8.0.0" + fast-safe-stringify "^2.0.6" + "@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" @@ -2771,6 +2842,29 @@ semver "^7.3.8" superstruct "^1.0.3" +"@metamask/utils@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.2.tgz#140ba5061d90d9dac0280c19cab101bc18c8857c" + integrity sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g== + dependencies: + "@ethereumjs/tx" "^4.1.2" + "@types/debug" "^4.1.7" + debug "^4.3.4" + semver "^7.3.8" + superstruct "^1.0.3" + +"@metamask/utils@^8.0.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.1.0.tgz#b8e73f5b4696b1b668cf5c1421daad140a3f98ac" + integrity sha512-sFNpzBKRicDgM2ZuU6vrPROlqNGm8/jDsjc5WrU1RzCkAMc4Xr3vUUf8p59uQ6B09etUWNb8d2GTCbISdmH/Ug== + dependencies: + "@ethereumjs/tx" "^4.1.2" + "@noble/hashes" "^1.3.1" + "@types/debug" "^4.1.7" + debug "^4.3.4" + semver "^7.5.4" + superstruct "^1.0.3" + "@motionone/animation@^10.15.1": version "10.15.1" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.15.1.tgz#4a85596c31cbc5100ae8eb8b34c459fb0ccf6807" @@ -3479,6 +3573,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@solana/buffer-layout@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -3829,11 +3928,489 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== +"@tkey-mpc/chrome-storage@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/chrome-storage/-/chrome-storage-8.2.0.tgz#32d765af196d19f471ecc00496150851bd4ef3ef" + integrity sha512-IY9ATAiqCWbuBB67nDsi0NSEgIpfA4Jxn0w1IMdgtnhG4pPX3E374Oy/D6Iz7q4cIVcRbkNAAYRM47J66V421g== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + webextension-polyfill "^0.10.0" + +"@tkey-mpc/common-types@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-8.2.0.tgz#844e4acc5aa45383deb16b2b49b9851abe944bbb" + integrity sha512-PvXAzW6FIQE8h+fgmSmJVKnvesXNygHrlaZ1sxmPWTB7uCgl48r38fSz8sdOv+yHhz7sTusQ67J2kXxQfXeOBg== + dependencies: + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/rss-client" "^1.5.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + serialize-error "^8.1.0" + ts-custom-error "^3.3.1" + web3-utils "^1.8.1" + +"@tkey-mpc/core@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/core/-/core-8.2.0.tgz#1f6b46ae1a6bc0340b5e1c6e64ce18c0a75f9b26" + integrity sha512-UGJgcr8dy+231MYsaB5bWDu8j7J4iYwrvqLXbKVdArYM/XDOFjMUrm+xgu4W1T0tmDGOE2gEyFb73gfPTDH+cg== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/http-helpers" "^4.0.0" + "@toruslabs/rss-client" "^1.5.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + json-stable-stringify "^1.0.2" + web3-utils "^1.8.1" + +"@tkey-mpc/security-questions@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/security-questions/-/security-questions-8.2.0.tgz#ba7c79095ae87804009cd2dab14130b6997084d7" + integrity sha512-kMaNzp63n3ox5f82a4sP8UNWhrezeMndZDORYOrw711YmlXSrdYToZ+eDPCL0d89LSJdwT/be/a3fXX+mBPYdg== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + bn.js "^5.2.1" + web3-utils "^1.8.1" + +"@tkey-mpc/service-provider-base@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-base/-/service-provider-base-8.2.0.tgz#925e414b7628bb665bafd49f28a5575285960a8c" + integrity sha512-DfQS/DJKetvMiL6OVCRiv4KHaqT/SkXBX+8Beyn5tn/ZsTAi6/zmOcXaWgSIdY+zaylBx1B4XtG81On9O5Dw1A== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + +"@tkey-mpc/service-provider-torus@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-torus/-/service-provider-torus-8.2.0.tgz#50d55e007213b2d36cb3702a59feafd6cc90266e" + integrity sha512-I9tpTqpobzwtJ2nYxYxVh27SMUwj3YQtN9r4N/rwb5J/3X5SdAQl1Ha+djItvlU7dqeKiwFiCMSwW+qdhQa4cA== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/service-provider-base" "^8.2.0" + "@toruslabs/customauth" "^15.0.3" + "@toruslabs/torus.js" "^10.0.5" + bn.js "^5.2.1" + elliptic "^6.5.4" + +"@tkey-mpc/share-serialization@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/share-serialization/-/share-serialization-8.2.0.tgz#7a29d2428dc40c6603ff038b46d48704f301ac6b" + integrity sha512-anH4ngxT9XZGUqF7gsk7hicau2rbIfxLAm8zh18eoa7ncRuBw+MKpuabneUepxnvlp2otPdN7arJ+LpRjw2DLQ== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + "@types/create-hash" "1.2.2" + bn.js "^5.2.1" + create-hash "^1.2.0" + +"@tkey-mpc/storage-layer-torus@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@tkey-mpc/storage-layer-torus/-/storage-layer-torus-8.2.0.tgz#67fe17f1186e8a0edd1bc0d271e3496a3fcdfa66" + integrity sha512-btx2668PTnsbLzDuPBLJwpJuB7rd6R5PjLixCosPOKjV3371Jp6CeeSnkQSjjeEnCRFThz9D66ttSjgwICdPPg== + dependencies: + "@tkey-mpc/common-types" "^8.2.0" + "@toruslabs/http-helpers" "^4.0.0" + bn.js "^5.2.1" + json-stable-stringify "^1.0.2" + web3-utils "^1.8.1" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@toruslabs/base-controllers@^2.2.6": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-2.9.0.tgz#e23f4228b5a90bf94ba9b0b27451f3024bd1acc4" + integrity sha512-rKc+bR4QB/wdbH0CxLZC5e2PUZcIgkr9yY7TMd3oIffDklaYBnsuC5ES2/rgK1aRUDRWz+qWbTwLqsY6PlT37Q== + dependencies: + "@ethereumjs/util" "^8.0.6" + "@toruslabs/broadcast-channel" "^6.2.0" + "@toruslabs/http-helpers" "^3.3.0" + "@toruslabs/openlogin-jrpc" "^4.0.0" + async-mutex "^0.4.0" + bignumber.js "^9.1.1" + bowser "^2.11.0" + eth-rpc-errors "^4.0.3" + json-rpc-random-id "^1.0.1" + lodash "^4.17.21" + loglevel "^1.8.1" + +"@toruslabs/base-controllers@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-4.0.1.tgz#877adfc391513e84c810917501c5610ae7af5338" + integrity sha512-6r9iUVdNsOR/oW2WSwrGZwPWDrMV4dt6lPOlzUbk+5vu+ijZ56jTlerUMHatcuHXuspISXXsJ9XhW64YoBTpRg== + dependencies: + "@ethereumjs/util" "^9.0.0" + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/broadcast-channel" "^8.0.0" + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/openlogin-jrpc" "^5.0.2" + async-mutex "^0.4.0" + bignumber.js "^9.1.2" + bowser "^2.11.0" + lodash "^4.17.21" + loglevel "^1.8.1" + +"@toruslabs/base-session-manager@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/base-session-manager/-/base-session-manager-3.0.0.tgz#4302d0747ef71a8278af79e577cf53253c907cd5" + integrity sha512-+EqwizmSFkVEczUtaw+swbAxRIIxC/EaFE040rwfgC5fixaQMNLw2cVYXWN67Ra47wC9A7Om6xwQTuGFR+dy4w== + dependencies: + "@toruslabs/http-helpers" "^5.0.0" + +"@toruslabs/broadcast-channel@^6.2.0": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-6.3.1.tgz#d4b0a08c3a0fa88d42d7f33387ce9be928c2d4b2" + integrity sha512-BEtJQ+9bMfFoGuCsp5NmxyY+C980Ho+3BZIKSiYwRtl5qymJ+jMX5lsoCppoQblcb34dP6FwEjeFw80Y9QC/rw== + dependencies: + "@babel/runtime" "^7.21.0" + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/metadata-helpers" "^3.2.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.6.1" + unload "^2.4.1" + +"@toruslabs/broadcast-channel@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-7.0.0.tgz#2e7f61cc0dceb16a6269a09f60a906f83462e26c" + integrity sha512-lyt2Els735qUXczPk743aHbIXZn+TeK/7YqKpUrY+xTyvXs9JyE6RahW+EY4yU90KAJx8+N9ZXZlcJP617T4bg== + dependencies: + "@babel/runtime" "^7.21.5" + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/metadata-helpers" "^4.0.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.6.1" + unload "^2.4.1" + +"@toruslabs/broadcast-channel@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-8.0.0.tgz#904ee44fceeb861d16f282ca211d1e2adf16e2f8" + integrity sha512-qCyWsHVL4Xtx1J6k1+acD7TJKCelJWyUy5Q5zyiWMPxMGFxTv1XdRyqpzV+VgwbcslIqgFN0GewOry2l1jlUQQ== + dependencies: + "@babel/runtime" "^7.22.10" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.7.2" + unload "^2.4.1" + +"@toruslabs/constants@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/constants/-/constants-11.0.0.tgz#c4a0ed1528ec5bbd6f9d1c4cd415913c693034e2" + integrity sha512-ImSw2cFMCdJH2zZ9jAR2vfK03YXpEGxBuNctx2Mq7fDyx+h0FvFSOV/VHMcjKcYCsPZyw1WjRCP0rDVY+7lHZg== + +"@toruslabs/constants@^13.0.1": + version "13.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/constants/-/constants-13.0.1.tgz#c08bbf21f4efaad37cf5311d52b9ab9964aa8b9b" + integrity sha512-haFppvgyHfKl2uTQKkrWOkgQmLgzbqxhIvaNvRGei4FgFNJNLr5+ju8/PwwbgKhQUi7adkeU0pjwFYITJyHEPw== + +"@toruslabs/customauth@^15.0.3": + version "15.0.5" + resolved "https://registry.yarnpkg.com/@toruslabs/customauth/-/customauth-15.0.5.tgz#f1c4ac0885662ca134213946fa10508cccb4fda8" + integrity sha512-qTbzvGjQyOZPvIlf2SEFj0dDhqc7ayd4ifZTa6ElmmoxrFLMdhnT4ckQrnS9T1w+y849kf7D+ijcsOSbLXErtg== + dependencies: + "@chaitanyapotti/register-service-worker" "^1.7.3" + "@toruslabs/broadcast-channel" "^7.0.0" + "@toruslabs/constants" "^11.0.0" + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/fetch-node-details" "^11.0.1" + "@toruslabs/http-helpers" "^4.0.0" + "@toruslabs/metadata-helpers" "^4.0.0" + "@toruslabs/torus.js" "^10.0.5" + bowser "^2.11.0" + events "^3.3.0" + jwt-decode "^3.1.2" + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/customauth@^16.0.4": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@toruslabs/customauth/-/customauth-16.0.4.tgz#d5ce64d3b0401be75b27c2e096f8f8ed7cbc33ed" + integrity sha512-vi4BJQk5J30e/5OoKrl25aYma0diZxcd0EXKz7ywYiN7nMrkCoqqkAls8uHPbpz3YinkNW3TBePUa9pUI7Eb4A== + dependencies: + "@chaitanyapotti/register-service-worker" "^1.7.3" + "@toruslabs/broadcast-channel" "^8.0.0" + "@toruslabs/constants" "^13.0.1" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/fetch-node-details" "^13.0.1" + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + "@toruslabs/torus.js" "^11.0.5" + bowser "^2.11.0" + events "^3.3.0" + jwt-decode "^3.1.2" + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/eccrypto@4.0.0", "@toruslabs/eccrypto@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-4.0.0.tgz#0b27ed2d1e9483e77f42a7619a2c3c19cb802f44" + integrity sha512-Z3EINkbsgJx1t6jCDVIJjLSUEGUtNIeDjhMWmeDGOWcP/+v/yQ1hEvd1wfxEz4q5WqIHhevacmPiVxiJ4DljGQ== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/eccrypto@^2.1.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-2.2.1.tgz#19012cc4e774e8c3df7ceebb2c1a07ecfd784917" + integrity sha512-7sviL0wLYsfA5ogEAOIdb0tu/QAOFXfHc9B8ONYtF04x4Mg3Nr89LL35FhjaEm055q8Ru7cUQhEFSiqJqm9GCw== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/eccrypto@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-3.0.0.tgz#96df18e9d7320b230492671cf2942e703cd34ae3" + integrity sha512-+lFjV+0FZ3S4zH5T/Gnc795HoqpzLLDW28fSkibZRlx1Nx8uwbl3pyJSfya0C0bRHH1/+NTeBogUDijaRJ1NCw== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/fetch-node-details@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-11.0.1.tgz#fb9542de5715a9bda5857fd47b5f38668e1ce29f" + integrity sha512-nkKWEDCBYinbLSWsL6ggbS4cdmis3Pk/SuVAzx29dhSaqW58QG+90KuvGqbiw42T8qaqC6kGtUmje7GOroCFgA== + dependencies: + "@toruslabs/constants" "^11.0.0" + "@toruslabs/fnd-base" "^11.0.1" + "@toruslabs/http-helpers" "^4.0.0" + loglevel "^1.8.1" + +"@toruslabs/fetch-node-details@^13.0.1": + version "13.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-13.0.1.tgz#4c9c4af44b3cdc961e36c84ce9af6886e1a0bf11" + integrity sha512-05WfFlrkIibO5VdYdx5+B70owzasXzRMSm19KYF/Iw/ch3jbRIC10tKtQMs2wGfpcMX+ngmGtlRYndUcy1UikA== + dependencies: + "@toruslabs/constants" "^13.0.1" + "@toruslabs/fnd-base" "^13.0.1" + "@toruslabs/http-helpers" "^5.0.0" + loglevel "^1.8.1" + +"@toruslabs/fnd-base@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/fnd-base/-/fnd-base-11.0.1.tgz#7a8a5e733e3e59596e1885a315bcd424de1d7ce2" + integrity sha512-Dd3r5xXeyODpSA03AuD91iZnXVS2GAQlhr8zv6WaNRoX5PCo0+7VXA8e/DnPdpEWtwL9GTKpdulr5ShksM128w== + dependencies: + "@toruslabs/constants" "^11.0.0" + +"@toruslabs/fnd-base@^13.0.1": + version "13.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/fnd-base/-/fnd-base-13.0.1.tgz#6f45dfee8a41984be7c38da45f474f2ab10d4e60" + integrity sha512-HJE+NAFBWIZmk9DN38n71epbdqulIc2lJDb2UU+RMqYh4YArjab4JRbBew2rye0SDyhCXSIAw4GHjEH9UYi9Zg== + dependencies: + "@toruslabs/constants" "^13.0.1" + +"@toruslabs/http-helpers@^3.2.0", "@toruslabs/http-helpers@^3.3.0", "@toruslabs/http-helpers@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-3.4.0.tgz#6d1da9e6aba094af62e73cf639a69844c82202f3" + integrity sha512-CoeJSL32mpp0gmYjxv48odu6pfjHk/rbJHDwCtYPcMHAl+qUQ/DTpVOOn9U0fGkD+fYZrQmZbRkXFgLhiT0ajQ== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/http-helpers@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-4.0.0.tgz#ca3341b7adb5f6795ded54967814850a9ff76d38" + integrity sha512-ef/Svevk54JANOn3Kf6UPf8X/vZlYHrusNFt8VV/LLahhVNXCXEcO8goC1bHkecu/u20CUyo9HJa0pn8fHh1sg== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/http-helpers@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-5.0.0.tgz#2a309d2a2c5c00d50a725d83ccec8a7475771d62" + integrity sha512-GmezWz9JeF6YyhjLSm+9XDF4YaeICEckY0Jbo43i86SjhfJYgRWqEi63VSiNsaqc/z810Q0FQvEk1TnBRX2tgA== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/metadata-helpers@5.0.0", "@toruslabs/metadata-helpers@^5.0.0", "@toruslabs/metadata-helpers@^5.x": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-5.0.0.tgz#12be5de4e8a5d1af2dd080bdf05f5ad8953aaae7" + integrity sha512-ZUFfOHJVJC53c8wJYHjdF3bIgN2ZvfqehbTZ/zJ7oVFfrrd6O66V3gQ1i1zxBjH3yhOvZKQwc0DaMmh3G0NUXQ== + dependencies: + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/http-helpers" "^5.0.0" + elliptic "^6.5.4" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + +"@toruslabs/metadata-helpers@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-3.2.0.tgz#b297933ac37481a9c86a125ac6a4e5c2f109fb78" + integrity sha512-2bCc6PNKd9y+aWfZQ1FXd47QmfyT4NmmqPGfsqk+sQS2o+MlxIyLuh9uh7deMgXo4b4qBDX+RQGbIKM1zVk56w== + dependencies: + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/http-helpers" "^3.4.0" + elliptic "^6.5.4" + ethereum-cryptography "^2.0.0" + json-stable-stringify "^1.0.2" + +"@toruslabs/metadata-helpers@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-4.0.1.tgz#fe91ca8fd45deece2a0b930f329b62c46a8a2fdd" + integrity sha512-0DFPxaNqmuVwFaEddl94dR/rpin5X+Odl1HR8cnzcrbzjLrOuKkGOdWtB6gnIqCUD6onMFO3156crgbHvagrLg== + dependencies: + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/http-helpers" "^4.0.0" + elliptic "^6.5.4" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + +"@toruslabs/openlogin-jrpc@^2.6.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-2.13.0.tgz#aae71e7c9b0161bc14baf3fc696605d74e0b99f4" + integrity sha512-TEg50/84xSocHLb3MEtw0DaIa+bXU66TJJjjDrqGPjoRo97fn8F8jDW2AcVV+eug39xpfxPIw1FFdCtgunmz7w== + dependencies: + "@toruslabs/openlogin-utils" "^2.13.0" + end-of-stream "^1.4.4" + eth-rpc-errors "^4.0.3" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^3.6.0" + +"@toruslabs/openlogin-jrpc@^4.0.0": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-4.7.2.tgz#e04dd6945da92d790f713a58aaa1657c57b330c8" + integrity sha512-9Eb0cPc0lPuS6v2YkQlgzfbRnZ6fLez9Ike5wznoHSFA2/JVu1onwuI56EV1HwswdDrOWPPQEyzI1j9NriZ0ew== + dependencies: + "@metamask/rpc-errors" "^5.1.1" + "@toruslabs/openlogin-utils" "^4.7.0" + end-of-stream "^1.4.4" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^4.4.2" + +"@toruslabs/openlogin-jrpc@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-5.0.2.tgz#a41a94f9f7bc6e33c0b73a908ad4081bbc742550" + integrity sha512-ezpCrQ+oh0wfEuq3GTdjTpANVmfNjG7B/m03IGwnnSpg0NJRWUZBp/sB+z9Q1cOYtUl1Kg1+8mfqdX69eAn8bg== + dependencies: + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/openlogin-utils" "^5.0.2" + end-of-stream "^1.4.4" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^4.4.2" + +"@toruslabs/openlogin-session-manager@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-session-manager/-/openlogin-session-manager-3.0.0.tgz#2f13af3ef96ddda48803a0265a798cffc3c9f78c" + integrity sha512-S+nnZQ+Y+XCHvTYaov3ltiV2hAAPpKpwxvB4TmbMvi7KWOZ8BcUJQykSITlIXV4aE5y5BD96rsmjQ3C3MyVtUQ== + dependencies: + "@toruslabs/base-session-manager" "^3.0.0" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "5.0.0" + +"@toruslabs/openlogin-utils@^2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-2.13.0.tgz#e339f9d638b1e3a8ecca7b8c973d6060a19afda5" + integrity sha512-g4pj6hIdKcuyetVsUWqiAJmCooTS9hOADL31m7LTqgdXzX9oR437A+c8Dw8gzFVcHmkK16Yt2//GvlKnSsGILg== + dependencies: + base64url "^3.0.1" + keccak "^3.0.3" + randombytes "^2.1.0" + +"@toruslabs/openlogin-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-4.7.0.tgz#741d6ba1c0754b59a182b1c6dd8d0263695ed980" + integrity sha512-w6XkHs4WKuufsf/zzteBzs4EJuOknrUmJ+iv5FZ8HzIpMQeL/984CP8HYaFSEYkbGCP4ydAnhY4Uh0QAhpDbPg== + dependencies: + base64url "^3.0.1" + +"@toruslabs/openlogin-utils@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-5.0.2.tgz#228ec74ce6ff363eb8856db636f8f971c783ad80" + integrity sha512-iOYnbowda0kk1Wk7Ax6xwCg5kVG8HDyq9VBKJfiyQeZCnSwiWW03UHL+UTCtiXy8jhBtkd7OEwh2Np2Lm30Tgg== + dependencies: + "@toruslabs/constants" "^13.0.1" + base64url "^3.0.1" + +"@toruslabs/openlogin@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin/-/openlogin-5.0.3.tgz#f07ba09ff96dbf855def3a89b86e6485b0684564" + integrity sha512-a7wdW/n5UINk1Uxn/hU4majTyIdeCk9XlSnmdr/ob71Dozz2hqPbXm1PFJ+XOMdlIPwqg4fLaOff6AySM1fqHg== + dependencies: + "@toruslabs/broadcast-channel" "^8.0.0" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + "@toruslabs/openlogin-session-manager" "^3.0.0" + "@toruslabs/openlogin-utils" "^5.0.2" + bowser "^2.11.0" + events "^3.3.0" + loglevel "^1.8.1" + ts-custom-error "^3.3.1" + +"@toruslabs/rss-client@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@toruslabs/rss-client/-/rss-client-1.5.0.tgz#82a02736f671152e1a4c9078194320e2bbc4e49e" + integrity sha512-7oaRZjmomNoL0+OZMFe8mv3hSeaYWvrxTjuUtkOHybMitStEcEj64tPjWia646BX7AFeqtUCURNdNaQzqfwDMQ== + dependencies: + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/http-helpers" "^3.2.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + fetch "^1.1.0" + loglevel "^1.8.1" + node-fetch "^2.0.0" + web3-eth-contract "^1.8.1" + web3-utils "^1.8.1" + +"@toruslabs/torus.js@^10.0.5": + version "10.0.5" + resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-10.0.5.tgz#32f7512cb70dee0284c1b1939b638522b1230060" + integrity sha512-dP2C1k/wxGFE+JrMavsFKucupPXjIBQwtaU++ENhsBGqkT8DWv4rzjlwwQj//VMf6ayELV7Jrr7yfOhEAeDzug== + dependencies: + "@toruslabs/constants" "^11.0.0" + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/http-helpers" "^4.0.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + ethereum-cryptography "^2.0.0" + json-stable-stringify "^1.0.2" + loglevel "^1.8.1" + +"@toruslabs/torus.js@^11.0.5": + version "11.0.5" + resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-11.0.5.tgz#89f6190ce004f269cd5a0941af1c5be2eba71bb8" + integrity sha512-CrC7foZB/2HrvMN5qVI4mEpPbmRAX5/2cdn3pgvgxWpEXQTNnJcxIO8vb2eT7iO6GhBYADnMZyxxWPMdk8S11Q== + dependencies: + "@toruslabs/constants" "^13.0.1" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/http-helpers" "^5.0.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + loglevel "^1.8.1" + +"@toruslabs/tss-client@^1.6.1-alpha.0": + version "1.6.1-alpha.0" + resolved "https://registry.yarnpkg.com/@toruslabs/tss-client/-/tss-client-1.6.1-alpha.0.tgz#876cdc3c1cae0889a29ad3ed7bf6460bf5055ee2" + integrity sha512-48nYPc/8v2LkbtgBJw3GRLM0Eb08ASFyK5i3nEDmLg1np7rDn/9TMovPckIKZNsqAb4Rwnsb6FmTNszg3UxYfw== + dependencies: + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/tss-lib" "^1.6.0-alpha.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + keccak256 "^1.0.6" + socket.io-client "^4.5.1" + +"@toruslabs/tss-lib@^1.6.0-alpha.0": + version "1.6.0-alpha.0" + resolved "https://registry.yarnpkg.com/@toruslabs/tss-lib/-/tss-lib-1.6.0-alpha.0.tgz#3c896c3cd7cec04bc66f4bb240f65c5f4ffe4400" + integrity sha512-sCkAFRZYMDYDWDkMEo73fyz1NnPdXZTRiQWaUCywmtV8fWQBFgneq3bVZGIMAhUNYY62PD0BubW3XaV6mqMYTg== + "@trezor/analytics@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@trezor/analytics/-/analytics-1.0.5.tgz#a30d6b836cacd12b69848e4044733030507a00e5" @@ -4103,6 +4680,13 @@ dependencies: "@types/node" "*" +"@types/create-hash@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/create-hash/-/create-hash-1.2.2.tgz#e87247083df8478f6b83655592bde0d709028235" + integrity sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.7": version "4.1.8" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" @@ -4882,7 +5466,7 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@^1.8.0": +"@walletconnect/types@^1.8.0", "@walletconnect/types@~1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== @@ -5074,6 +5658,103 @@ joi "17.9.1" rxjs "^7.5.2" +"@web3auth-mpc/base-provider@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/base-provider/-/base-provider-2.1.9.tgz#407aca075deb981b5cae80567828526aecf385d7" + integrity sha512-i3NmTDk8ezaZXz+wtR1O931DBq9cDfyCzYI/XHFHA0zS0iealCSHXXKD6iACByoUuFDBNSq7GdgZCJNcoqWOGw== + dependencies: + "@toruslabs/base-controllers" "^2.2.6" + "@toruslabs/openlogin-jrpc" "^2.6.0" + "@web3auth-mpc/base" "^2.1.9" + eth-rpc-errors "^4.0.3" + json-rpc-random-id "^1.0.1" + +"@web3auth-mpc/base@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/base/-/base-2.1.9.tgz#1d1768f5b7c955c5a7e8e71a6590aeee49352604" + integrity sha512-T91A+1Vu4C3//5Hj94bhMUaerOu3nVwGfELON2FH16yXNPQsNZPwyBuBoROUYdTp4fOZvCG4OtLiw+g8dPncaA== + dependencies: + "@toruslabs/http-helpers" "^3.2.0" + "@toruslabs/openlogin-jrpc" "^2.6.0" + jwt-decode "^3.1.2" + loglevel "^1.8.0" + ts-custom-error "^3.2.2" + +"@web3auth-mpc/ethereum-provider@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/ethereum-provider/-/ethereum-provider-2.3.0.tgz#000e83696ee6894879a4a8aaef2fd2df5ae3cc6c" + integrity sha512-2RY8GPd2UJ04P4Gzjt+Uec00j721LIceSJoHlwhOPGYNPx0QpRsAmwOmMRNLBA5Y+beKrnQ8b8rGDF9wBUaqtA== + dependencies: + "@ethereumjs/common" "^3.0.0" + "@ethereumjs/rlp" "^4.0.0" + "@ethereumjs/tx" "^4.0.0" + "@ethereumjs/util" "^8.0.0" + "@metamask/eth-sig-util" "^5.0.0" + "@toruslabs/base-controllers" "^2.2.6" + "@toruslabs/http-helpers" "^3.2.0" + "@toruslabs/openlogin-jrpc" "^2.6.0" + "@walletconnect/types" "~1.8.0" + "@web3auth-mpc/base" "^2.1.9" + "@web3auth-mpc/base-provider" "^2.1.9" + assert "^2.0.0" + bignumber.js "^9.1.0" + bn.js "^5.2.1" + eth-rpc-errors "^4.0.3" + jsonschema "^1.4.1" + +"@web3auth/base-provider@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@web3auth/base-provider/-/base-provider-7.0.1.tgz#6fdb377a8b9e2ca26419020e74a2c495ef95c1ea" + integrity sha512-8ggJtsD/1MbpMEbe+uBSS3mrqE/nqEKpRLyIjuyfobPs4Zu/VI2Exdjz6wcUIzwEB1iYyjFt29/gyIXufMyCgA== + dependencies: + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/base-controllers" "^4.0.1" + "@toruslabs/openlogin-jrpc" "^5.0.2" + "@web3auth/base" "^7.0.1" + json-rpc-random-id "^1.0.1" + +"@web3auth/base@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@web3auth/base/-/base-7.0.1.tgz#2116b5de8914ec4c6547a60bf1caf90428bca39f" + integrity sha512-6ft4XAD37tobOxgkZhideiBMqoVJTpvAvYD0R99IDmpazpIrX8b4RBgwLQ/gIlNmGZhkU1UoTzub19xTWI9h6w== + dependencies: + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/openlogin" "^5.0.3" + "@toruslabs/openlogin-jrpc" "^5.0.2" + "@toruslabs/openlogin-utils" "^5.0.2" + jwt-decode "^3.1.2" + loglevel "^1.8.1" + ts-custom-error "^3.3.1" + +"@web3auth/mpc-core-kit@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.0.1.tgz#0a79165bcff1925d57463a10793a5323dd9141b2" + integrity sha512-TTr8bM+5SkcmGE94DdzxnmuTsO2QzTojlxF03Tj6KCWNDOIav+nj94m2oErxG8vB9JY6L5IfKc8/d+igP8F1Gg== + dependencies: + "@tkey-mpc/chrome-storage" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/core" "^8.2.0" + "@tkey-mpc/security-questions" "^8.2.0" + "@tkey-mpc/service-provider-torus" "^8.2.0" + "@tkey-mpc/share-serialization" "^8.2.0" + "@tkey-mpc/storage-layer-torus" "^8.2.0" + "@toruslabs/constants" "^13.0.1" + "@toruslabs/customauth" "^16.0.4" + "@toruslabs/eccrypto" "4.0.0" + "@toruslabs/fetch-node-details" "^13.0.1" + "@toruslabs/fnd-base" "^13.0.1" + "@toruslabs/metadata-helpers" "^5.x" + "@toruslabs/openlogin-session-manager" "^3.0.0" + "@toruslabs/torus.js" "^11.0.5" + "@toruslabs/tss-client" "^1.6.1-alpha.0" + "@toruslabs/tss-lib" "^1.6.0-alpha.0" + "@web3auth-mpc/ethereum-provider" "^2.3.0" + "@web3auth/base" "^7.0.1" + "@web3auth/base-provider" "^7.0.1" + bn.js "^5.2.1" + bowser "^2.11.0" + elliptic "^6.5.4" + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -5092,6 +5773,13 @@ abitype@0.9.3: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.3.tgz#294d25288ee683d72baf4e1fed757034e3c8c277" integrity sha512-dz4qCQLurx97FQhnb/EIYTk/ldQ+oafEDUqC0VVIeQS1Q48/YWt/9YNfMmp9SLFqN41ktxny3c8aYxHjmFIB/w== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + abortcontroller-polyfill@^1.7.3: version "1.7.5" resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" @@ -5445,6 +6133,13 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -5664,6 +6359,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + bchaddrjs@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.5.2.tgz#1f52b5077329774e7c82d4882964628106bb11a0" @@ -5723,6 +6423,11 @@ bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.1.0, bignumber.js@^9.1 resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== +bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -5747,6 +6452,13 @@ bip66@^1.1.5: dependencies: safe-buffer "^5.0.1" +biskviit@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7" + integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w== + dependencies: + psl "^1.1.7" + bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" @@ -5786,7 +6498,7 @@ bn.js@4.11.8: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -6784,7 +7496,7 @@ debug@2.6.9, debug@^2.2.0: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7163,13 +7875,36 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +encoding@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA== + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" + integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + enhanced-resolve@^5.12.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -7816,7 +8551,7 @@ eth-rpc-errors@^3.0.0: dependencies: fast-safe-stringify "^2.0.6" -eth-rpc-errors@^4.0.2: +eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== @@ -8154,6 +8889,11 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter2@6.4.7: version "6.4.7" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" @@ -8363,7 +9103,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== -fast-safe-stringify@^2.0.6: +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -8394,6 +9134,14 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fetch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e" + integrity sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA== + dependencies: + biskviit "1.0.1" + encoding "0.1.12" + figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -9109,7 +9857,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -10288,6 +11036,11 @@ jsonschema@1.2.2: resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" integrity sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA== +jsonschema@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -10323,6 +11076,20 @@ jsqr@^1.2.0: object.assign "^4.1.4" object.values "^1.1.6" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + +keccak256@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" + integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== + dependencies: + bn.js "^5.2.0" + buffer "^6.0.3" + keccak "^3.0.2" + keccak@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" @@ -10332,7 +11099,7 @@ keccak@3.0.2: node-gyp-build "^4.2.0" readable-stream "^3.6.0" -keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.3: +keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.2, keccak@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.3.tgz#4bc35ad917be1ef54ff246f904c2bbbf9ac61276" integrity sha512-JZrLIAJWuZxKbCilMpNz5Vj7Vtb4scDG3dMXLOsbzBmQGyjwE61BbW7bJkfKKCShXiQZt3T6sBgALRtmd+nZaQ== @@ -10603,6 +11370,11 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +loglevel@^1.8.0, loglevel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -11129,6 +11901,13 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== +node-fetch@^2.0.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -11276,6 +12055,11 @@ object.values@^1.1.6: define-properties "^1.2.0" es-abstract "^1.22.1" +oblivious-set@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" + integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== + oboe@2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/oboe/-/oboe-2.1.5.tgz#5554284c543a2266d7a38f17e073821fbde393cd" @@ -11822,7 +12606,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.28, psl@^1.1.33, psl@^1.1.7: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -12176,6 +12960,17 @@ readable-stream@^3.1.1, readable-stream@^3.5.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.2.tgz#e6aced27ad3b9d726d8308515b9a1b98dc1b9d13" + integrity sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~1.0.15, readable-stream@~1.0.17, readable-stream@~1.0.27-1: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -12796,6 +13591,13 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +serialize-error@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -12953,6 +13755,24 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +socket.io-client@^4.5.1, socket.io-client@^4.6.1, socket.io-client@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" + integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + socks-proxy-agent@6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" @@ -13192,7 +14012,7 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -13635,6 +14455,11 @@ ts-command-line-args@^2.2.0: command-line-usage "^6.1.0" string-format "^2.0.0" +ts-custom-error@^3.2.2, ts-custom-error@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" + integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== + ts-essentials@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" @@ -13973,6 +14798,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unload@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.4.1.tgz#b0c5b7fb44e17fcbf50dcb8fb53929c59dd226a5" + integrity sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -14288,7 +15118,7 @@ web3-core@1.10.0: web3-core-requestmanager "1.10.0" web3-utils "1.10.0" -web3-core@^1.8.1: +web3-core@1.10.1, web3-core@^1.8.1: version "1.10.1" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.1.tgz#a4cb471356c4a197654b61adc9d0b03357da4258" integrity sha512-a45WF/e2VeSs17UTmmWhEaMDv/A+N6qchA7zepvdvwUGCZME39YWCmbsjAYjkq0btsXueOIBpS6fLuq5VoLkFg== @@ -14309,6 +15139,14 @@ web3-eth-abi@1.10.0: "@ethersproject/abi" "^5.6.3" web3-utils "1.10.0" +web3-eth-abi@1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.1.tgz#4d64d1d272f35f7aaae7be9679493474e0af86c7" + integrity sha512-hk5NyeGweJYTjes7lBW7gtG7iYoN6HLt6E4FQDrHPdwZjwNmvzaOH9N8zMTCxNFXUlg0bzeTOzWwMA717a+4eg== + dependencies: + "@ethersproject/abi" "^5.6.3" + web3-utils "1.10.1" + web3-eth-accounts@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz#2942beca0a4291455f32cf09de10457a19a48117" @@ -14339,6 +15177,20 @@ web3-eth-contract@1.10.0: web3-eth-abi "1.10.0" web3-utils "1.10.0" +web3-eth-contract@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.1.tgz#8f8a514db8a1c81337e67a60054840edfc8c26bb" + integrity sha512-eRZItYq8LzSPOKqgkTaT1rRruXTNkjbeIe9Cs+VFx3+p/GHyUI1Rj4rfBXp1MBR6p4WK+oy05sB+FNugOYxe8Q== + dependencies: + "@types/bn.js" "^5.1.1" + web3-core "1.10.1" + web3-core-helpers "1.10.1" + web3-core-method "1.10.1" + web3-core-promievent "1.10.1" + web3-core-subscriptions "1.10.1" + web3-eth-abi "1.10.1" + web3-utils "1.10.1" + web3-eth-ens@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz#96a676524e0b580c87913f557a13ed810cf91cd9" @@ -14540,6 +15392,11 @@ web3@1.10.0: web3-shh "1.10.0" web3-utils "1.10.0" +webextension-polyfill@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" + integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -14981,6 +15838,11 @@ ws@^8.11.0, ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xhr-request-promise@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c" @@ -15021,6 +15883,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From 65dd3a464038726f41996895138547bbf59c99cf Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 21 Sep 2023 20:52:45 +0200 Subject: [PATCH 02/52] test: mpc onboard module --- jest.config.cjs | 18 +++- src/services/mpc/__tests__/module.test.ts | 118 ++++++++++++++++++++++ src/services/mpc/module.ts | 1 - 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 src/services/mpc/__tests__/module.test.ts diff --git a/jest.config.cjs b/jest.config.cjs index bd350fb152..8479044a87 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -8,6 +8,7 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest const customJestConfig = { setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { // Handle module aliases (this will be automatically configured for you soon) '^@/(.*)$': '/src/$1', @@ -16,9 +17,20 @@ const customJestConfig = { testEnvironment: 'jest-environment-jsdom', testEnvironmentOptions: { url: 'http://localhost/balances?safe=rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A' }, globals: { - fetch: global.fetch - } + fetch: global.fetch, + }, } // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -module.exports = createJestConfig(customJestConfig) +// Jest does not allow modification of transformIgnorePatterns within createJestConfig, therefore we add one entry after initializing the config +module.exports = async () => { + const jestConfig = await createJestConfig(customJestConfig)() + const existingTransformIgnorePatterns = jestConfig.transformIgnorePatterns.filter( + (pattern) => pattern !== '/node_modules/', + ) + + return { + ...jestConfig, + transformIgnorePatterns: [...existingTransformIgnorePatterns, '/node_modules/(?!(@web3-onboard/common)/)'], + } +} diff --git a/src/services/mpc/__tests__/module.test.ts b/src/services/mpc/__tests__/module.test.ts new file mode 100644 index 0000000000..4a510b992f --- /dev/null +++ b/src/services/mpc/__tests__/module.test.ts @@ -0,0 +1,118 @@ +import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../module' +import { type WalletModule } from '@web3-onboard/common' + +import * as web3 from '@/hooks/wallets/web3' +import * as useMPC from '@/hooks/wallets/mpc/useMPC' +import { hexZeroPad } from 'ethers/lib/utils' + +describe('MPC Onboard module', () => { + it('should return correct metadata', async () => { + const mpcModule = MpcModule()({ + device: { + browser: { + name: 'Firefox', + version: '1.0', + }, + os: { + name: 'macOS', + version: '1.0', + }, + type: 'desktop', + }, + }) + expect(Array.isArray(mpcModule)).toBeFalsy() + const walletModule = mpcModule as WalletModule + + expect(walletModule.label).toBe(ONBOARD_MPC_MODULE_LABEL) + expect(walletModule.getIcon()).toBeDefined() + const walletInterface = await walletModule.getInterface({} as any) + expect(walletInterface.instance).toBeUndefined() + expect(walletInterface.provider).toBeDefined() + }) + + it('should call web3readonly for eth_estimateGas', async () => { + const mockReadOnlySend = jest.fn().mockImplementation(() => Promise.resolve('0x5')) + jest.spyOn(web3, 'getWeb3ReadOnly').mockReturnValue({ + send: mockReadOnlySend, + } as any) + + jest.spyOn(useMPC, 'getMPCCoreKitInstance').mockImplementation(() => { + return { + provider: {}, + } as any + }) + + const mpcModule = MpcModule()({ + device: { + browser: { + name: 'Firefox', + version: '1.0', + }, + os: { + name: 'macOS', + version: '1.0', + }, + type: 'desktop', + }, + }) + const walletModule = mpcModule as WalletModule + const walletInterface = await walletModule.getInterface({} as any) + + await walletInterface.provider.request({ + method: 'eth_estimateGas', + params: [ + { + to: hexZeroPad('0x123', 20), + value: '0', + data: '0x', + }, + ], + }) + + expect(mockReadOnlySend).toHaveBeenCalledWith('eth_estimateGas', [ + { + to: hexZeroPad('0x123', 20), + value: '0', + data: '0x', + }, + ]) + }) + + it('should call eth_accounts when eth_requestAccounts gets called', async () => { + const mockReadOnlySend = jest.fn() + const mockMPCProviderRequest = jest.fn().mockImplementation(() => Promise.resolve(hexZeroPad('0x456', 20))) + jest.spyOn(web3, 'getWeb3ReadOnly').mockReturnValue({ + send: mockReadOnlySend, + } as any) + + jest.spyOn(useMPC, 'getMPCCoreKitInstance').mockImplementation(() => { + return { + provider: { + request: mockMPCProviderRequest, + }, + } as any + }) + + const mpcModule = MpcModule()({ + device: { + browser: { + name: 'Firefox', + version: '1.0', + }, + os: { + name: 'macOS', + version: '1.0', + }, + type: 'desktop', + }, + }) + const walletModule = mpcModule as WalletModule + const walletInterface = await walletModule.getInterface({} as any) + + await walletInterface.provider.request({ + method: 'eth_requestAccounts', + }) + + expect(mockMPCProviderRequest).toHaveBeenCalledWith({ method: 'eth_accounts' }) + }) +}) diff --git a/src/services/mpc/module.ts b/src/services/mpc/module.ts index 48ebc24668..180cbddc43 100644 --- a/src/services/mpc/module.ts +++ b/src/services/mpc/module.ts @@ -35,7 +35,6 @@ function MpcModule(): WalletInit { request: (request) => { return new Promise((resolve, reject) => { try { - console.log('MPC Module RPC Request', request) const web3 = assertDefined(getMPCProvider()) const web3ReadOnly = assertDefined(getWeb3ReadOnly()) /* From 19c447727407e1f8bd6e1666a4bd42e4ee200a32 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 22 Sep 2023 15:11:43 +0200 Subject: [PATCH 03/52] [Seedless-onboarding] - tests for useMPC hook (#2536) * test: mpc wallet session rehydration, chain switching, onboard connection --- .../wallets/mpc/__tests__/useMPC.test.ts | 225 ++++++++++++++++++ src/hooks/wallets/mpc/useMPC.ts | 58 ++--- 2 files changed, 256 insertions(+), 27 deletions(-) create mode 100644 src/hooks/wallets/mpc/__tests__/useMPC.test.ts diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts new file mode 100644 index 0000000000..8665b5ec9d --- /dev/null +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -0,0 +1,225 @@ +import * as useOnboard from '@/hooks/wallets/useOnboard' +import { renderHook, waitFor } from '@/tests/test-utils' +import { getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' +import * as useChains from '@/hooks/useChains' +import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' +import { hexZeroPad } from 'ethers/lib/utils' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { type Web3AuthMPCCoreKit, COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' + +jest.mock('@web3auth/mpc-core-kit', () => ({ + ...jest.requireActual('@web3auth/mpc-core-kit'), + Web3AuthMPCCoreKit: jest.fn(), +})) + +type MPCProvider = Web3AuthMPCCoreKit['provider'] + +/** + * Mock for creating and initializing the MPC Core Kit + */ +class MockMPCCoreKit { + provider: MPCProvider | null = null + status = COREKIT_STATUS.NOT_INITIALIZED + private mockState + private mockProvider + + /** + * The parameters are set in the mock MPC Core Kit after init() get's called + * + * @param mockState + * @param mockProvider + */ + constructor(mockState: COREKIT_STATUS, mockProvider: MPCProvider) { + this.mockState = mockState + this.mockProvider = mockProvider + } + + init() { + this.status = this.mockState + this.provider = this.mockProvider + return Promise.resolve() + } +} + +/** + * Small helper class that implements registering RPC event listeners and event emiting. + * Used to test that events onboard relies on are getting called correctly + */ +class EventEmittingMockProvider { + private chainChangedListeners: Function[] = [] + + addListener(event: string, listener: Function) { + if (event === 'chainChanged') { + this.chainChangedListeners.push(listener) + } + } + + emit(event: string, ...args: any[]) { + this.chainChangedListeners.forEach((listener) => listener(...args)) + } +} + +describe('useInitMPC', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + it('should set the coreKit if user is not logged in yet', async () => { + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) + jest.spyOn(useOnboard, 'default').mockReturnValue({ + state: { + get: () => ({ + wallets: [], + walletModules: [], + }), + }, + } as unknown as OnboardAPI) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ + chainId: '5', + chainName: 'Goerli', + blockExplorerUriTemplate: { + address: 'https://goerli.someprovider.io/{address}', + txHash: 'https://goerli.someprovider.io/{txHash}', + api: 'https://goerli.someprovider.io/', + }, + nativeCurrency: { + decimals: 18, + logoUri: 'https://logo.goerli.com', + name: 'Goerli ETH', + symbol: 'ETH', + }, + rpcUri: { + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: 'https://goerli.somerpc.io', + }, + } as unknown as ChainInfo) + + const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') + mockWeb3AuthMpcCoreKit.mockImplementation(() => { + return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, null) + }) + + renderHook(() => useInitMPC()) + + await waitFor(() => { + expect(getMPCCoreKitInstance()).toBeDefined() + expect(connectWalletSpy).not.toBeCalled() + }) + }) + + it('should call connectWallet after rehydrating a web3auth session', async () => { + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) + jest.spyOn(useOnboard, 'default').mockReturnValue({ + state: { + get: () => ({ + wallets: [], + walletModules: [], + }), + }, + } as unknown as OnboardAPI) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ + chainId: '5', + chainName: 'Goerli', + blockExplorerUriTemplate: { + address: 'https://goerli.someprovider.io/{address}', + txHash: 'https://goerli.someprovider.io/{txHash}', + api: 'https://goerli.someprovider.io/', + }, + nativeCurrency: { + decimals: 18, + logoUri: 'https://logo.goerli.com', + name: 'Goerli ETH', + symbol: 'ETH', + }, + rpcUri: { + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: 'https://goerli.somerpc.io', + }, + } as unknown as ChainInfo) + + const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') + const mockProvider = jest.fn() + mockWeb3AuthMpcCoreKit.mockImplementation(() => { + return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, mockProvider as unknown as MPCProvider) + }) + + renderHook(() => useInitMPC()) + + await waitFor(() => { + expect(connectWalletSpy).toBeCalled() + expect(getMPCCoreKitInstance()).toBeDefined() + }) + }) + + it('should copy event handlers and emit chainChanged if the current chain is updated', async () => { + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue({ + address: hexZeroPad('0x1', 20), + label: ONBOARD_MPC_MODULE_LABEL, + chainId: '1', + provider: {} as unknown as EIP1193Provider, + }) + jest.spyOn(useOnboard, 'default').mockReturnValue({ + state: { + get: () => ({ + wallets: [], + walletModules: [], + }), + }, + } as unknown as OnboardAPI) + jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ + chainId: '5', + chainName: 'Goerli', + blockExplorerUriTemplate: { + address: 'https://goerli.someprovider.io/{address}', + txHash: 'https://goerli.someprovider.io/{txHash}', + api: 'https://goerli.someprovider.io/', + }, + nativeCurrency: { + decimals: 18, + logoUri: 'https://logo.goerli.com', + name: 'Goerli ETH', + symbol: 'ETH', + }, + rpcUri: { + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: 'https://goerli.somerpc.io', + }, + } as unknown as ChainInfo) + + const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') + const mockChainChangedListener = jest.fn() + const mockProviderBefore = { + listeners: (eventName: string) => { + if (eventName === 'chainChanged') { + return [mockChainChangedListener] + } + }, + } + + setMPCCoreKitInstance({ + provider: mockProviderBefore, + } as unknown as Web3AuthMPCCoreKit) + + const mockProvider = new EventEmittingMockProvider() + mockWeb3AuthMpcCoreKit.mockImplementation(() => { + return new MockMPCCoreKit( + require('@web3auth/mpc-core-kit').COREKIT_STATUS.INITIALIZED, + mockProvider as unknown as MPCProvider, + ) + }) + + renderHook(() => useInitMPC()) + + await waitFor(() => { + expect(mockChainChangedListener).toHaveBeenCalledWith('0x5') + expect(getMPCCoreKitInstance()).toBeDefined() + expect(connectWalletSpy).not.toBeCalled() + }) + }) +}) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index cf1472ab13..b7d66c444f 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import ExternalStore from '@/services/ExternalStore' -import { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK, COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' import { CHAIN_NAMESPACES } from '@web3auth/base' import { WEB3_AUTH_CLIENT_ID } from '@/config/constants' @@ -29,6 +29,14 @@ export const useInitMPC = () => { tickerName: chain.nativeCurrency.name, } + const currentInstance = getStore() + let previousChainChangedListeners: Function[] = [] + if (currentInstance?.provider) { + // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId + const oldProvider = currentInstance.provider + previousChainChangedListeners = oldProvider.listeners('chainChanged') + } + const web3AuthCoreKit = new Web3AuthMPCCoreKit({ web3AuthClientId: WEB3_AUTH_CLIENT_ID, // Available networks are "sapphire_devnet", "sapphire_mainnet" @@ -43,33 +51,27 @@ export const useInitMPC = () => { .init() .then(() => { setStore(web3AuthCoreKit) - // If rehydration was successful, connect to onboard - if (web3AuthCoreKit.status === COREKIT_STATUS.INITIALIZED) { - console.log('Logged in', web3AuthCoreKit) - // await mpcCoreKit.enableMFA({}) - const connectedWallet = getConnectedWallet(onboard.state.get().wallets) - if (!connectedWallet) { - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - } else { - // To propagate the changedChain we disconnect and connect - onboard - .disconnectWallet({ - label: ONBOARD_MPC_MODULE_LABEL, - }) - .then(() => - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }), - ) + if (!web3AuthCoreKit.provider) { + return + } + const connectedWallet = getConnectedWallet(onboard.state.get().wallets) + if (!connectedWallet) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + } else { + const newProvider = web3AuthCoreKit.provider + + // To propagate the changedChain we disconnect and connect + if (previousChainChangedListeners.length > 0 && newProvider) { + previousChainChangedListeners.forEach((previousListener) => + newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), + ) + newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) } } }) @@ -79,4 +81,6 @@ export const useInitMPC = () => { export const getMPCCoreKitInstance = getStore +export const setMPCCoreKitInstance = setStore + export default useStore From 387b1565b06a6c65fa14ae569363906908e97410 Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 25 Sep 2023 09:35:17 +0200 Subject: [PATCH 04/52] fix: update mpc core sdk to 1.0.2 --- package.json | 2 +- .../wallets/mpc/__tests__/useMPC.test.ts | 7 +- src/hooks/wallets/mpc/useMPC.ts | 4 +- yarn.lock | 104 +++++++++--------- 4 files changed, 57 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 0e4a65836e..ab8f5ec26a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.5", - "@web3auth/mpc-core-kit": "^1.0.1", + "@web3auth/mpc-core-kit": "^1.0.2", "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.29.2", diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index 8665b5ec9d..3f67c0a1e7 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -144,7 +144,7 @@ describe('useInitMPC', () => { const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') const mockProvider = jest.fn() mockWeb3AuthMpcCoreKit.mockImplementation(() => { - return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, mockProvider as unknown as MPCProvider) + return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) renderHook(() => useInitMPC()) @@ -208,10 +208,7 @@ describe('useInitMPC', () => { const mockProvider = new EventEmittingMockProvider() mockWeb3AuthMpcCoreKit.mockImplementation(() => { - return new MockMPCCoreKit( - require('@web3auth/mpc-core-kit').COREKIT_STATUS.INITIALIZED, - mockProvider as unknown as MPCProvider, - ) + return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) renderHook(() => useInitMPC()) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index b7d66c444f..ba110255ea 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import ExternalStore from '@/services/ExternalStore' -import { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS, Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' import { CHAIN_NAMESPACES } from '@web3auth/base' import { WEB3_AUTH_CLIENT_ID } from '@/config/constants' @@ -52,7 +52,7 @@ export const useInitMPC = () => { .then(() => { setStore(web3AuthCoreKit) // If rehydration was successful, connect to onboard - if (!web3AuthCoreKit.provider) { + if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { return } const connectedWallet = getConnectedWallet(onboard.state.get().wallets) diff --git a/yarn.lock b/yarn.lock index f095bc68f9..a5f907194b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3928,18 +3928,18 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== -"@tkey-mpc/chrome-storage@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/chrome-storage/-/chrome-storage-8.2.0.tgz#32d765af196d19f471ecc00496150851bd4ef3ef" - integrity sha512-IY9ATAiqCWbuBB67nDsi0NSEgIpfA4Jxn0w1IMdgtnhG4pPX3E374Oy/D6Iz7q4cIVcRbkNAAYRM47J66V421g== +"@tkey-mpc/chrome-storage@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/chrome-storage/-/chrome-storage-8.2.2.tgz#bc36b0cb35035024fd6f06d58184473e9c5d502b" + integrity sha512-1hBpoVsS9JYAdOskzlRjBKpJGLQFhrany6Htuw6U90gNIq8sf5u3rGK/faIJr2w5dARm5dqSwAnW6qPGsMjH1A== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" webextension-polyfill "^0.10.0" -"@tkey-mpc/common-types@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-8.2.0.tgz#844e4acc5aa45383deb16b2b49b9851abe944bbb" - integrity sha512-PvXAzW6FIQE8h+fgmSmJVKnvesXNygHrlaZ1sxmPWTB7uCgl48r38fSz8sdOv+yHhz7sTusQ67J2kXxQfXeOBg== +"@tkey-mpc/common-types@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-8.2.2.tgz#194324a9ad92027c0f040896fa94fec096e83c5d" + integrity sha512-aN/LUXFh+t+4vf1zzlIIh3xd+M4HKzBgaJq6eVopPLMxaG941ZFyOq9Tbymzliq9lrFhA3ByEyPFI6Lunqzvbg== dependencies: "@toruslabs/eccrypto" "^3.0.0" "@toruslabs/rss-client" "^1.5.0" @@ -3949,12 +3949,12 @@ ts-custom-error "^3.3.1" web3-utils "^1.8.1" -"@tkey-mpc/core@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/core/-/core-8.2.0.tgz#1f6b46ae1a6bc0340b5e1c6e64ce18c0a75f9b26" - integrity sha512-UGJgcr8dy+231MYsaB5bWDu8j7J4iYwrvqLXbKVdArYM/XDOFjMUrm+xgu4W1T0tmDGOE2gEyFb73gfPTDH+cg== +"@tkey-mpc/core@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/core/-/core-8.2.2.tgz#1aeb306b2cfa5a4980f423870d4882d9ed8b53a0" + integrity sha512-uy1jmvyXpoStEWp3GhYzpe/QJrdekXDMNt6tcugrm1sYk2E4cUoqcIRjB/HyduaONvqQK4i2AyCCdhgFeRUfWw== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" "@toruslabs/eccrypto" "^3.0.0" "@toruslabs/http-helpers" "^4.0.0" "@toruslabs/rss-client" "^1.5.0" @@ -3963,52 +3963,52 @@ json-stable-stringify "^1.0.2" web3-utils "^1.8.1" -"@tkey-mpc/security-questions@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/security-questions/-/security-questions-8.2.0.tgz#ba7c79095ae87804009cd2dab14130b6997084d7" - integrity sha512-kMaNzp63n3ox5f82a4sP8UNWhrezeMndZDORYOrw711YmlXSrdYToZ+eDPCL0d89LSJdwT/be/a3fXX+mBPYdg== +"@tkey-mpc/security-questions@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/security-questions/-/security-questions-8.2.2.tgz#bec69becec6956e50c65c26b4d65a5a29886b170" + integrity sha512-wyZtUvapwft82ehDEpy/g/Qo/3L7PqIZCXl3WiijD2pAyh4D+10J+EnqU7gEY89itIZQR3U8Vfl9yl5QULiHaA== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" bn.js "^5.2.1" web3-utils "^1.8.1" -"@tkey-mpc/service-provider-base@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-base/-/service-provider-base-8.2.0.tgz#925e414b7628bb665bafd49f28a5575285960a8c" - integrity sha512-DfQS/DJKetvMiL6OVCRiv4KHaqT/SkXBX+8Beyn5tn/ZsTAi6/zmOcXaWgSIdY+zaylBx1B4XtG81On9O5Dw1A== +"@tkey-mpc/service-provider-base@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-base/-/service-provider-base-8.2.2.tgz#a94ec561aa88064e7171ea19a1733f345dfd8f91" + integrity sha512-U8Pfnm+iQy+f7eSBTd5li286fLc1+ltnLIi3bo1qADV/KcCGMgWEljLaNY+RZGO1SJdIg1rKCOvWkO/lj4hJiQ== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" bn.js "^5.2.1" elliptic "^6.5.4" -"@tkey-mpc/service-provider-torus@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-torus/-/service-provider-torus-8.2.0.tgz#50d55e007213b2d36cb3702a59feafd6cc90266e" - integrity sha512-I9tpTqpobzwtJ2nYxYxVh27SMUwj3YQtN9r4N/rwb5J/3X5SdAQl1Ha+djItvlU7dqeKiwFiCMSwW+qdhQa4cA== +"@tkey-mpc/service-provider-torus@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-torus/-/service-provider-torus-8.2.2.tgz#b57d51a7c596cc69d5c2236e204a08ac0059ddf4" + integrity sha512-Ocq6xaRGKbQ6M91Dq3T2ude9ZdCnA2s4itifxtRtg+R2sJdAQpYB4sgqbbmgNUX7CsDbjmuGvSJyi4Oobpgmkw== dependencies: - "@tkey-mpc/common-types" "^8.2.0" - "@tkey-mpc/service-provider-base" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" + "@tkey-mpc/service-provider-base" "^8.2.2" "@toruslabs/customauth" "^15.0.3" "@toruslabs/torus.js" "^10.0.5" bn.js "^5.2.1" elliptic "^6.5.4" -"@tkey-mpc/share-serialization@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/share-serialization/-/share-serialization-8.2.0.tgz#7a29d2428dc40c6603ff038b46d48704f301ac6b" - integrity sha512-anH4ngxT9XZGUqF7gsk7hicau2rbIfxLAm8zh18eoa7ncRuBw+MKpuabneUepxnvlp2otPdN7arJ+LpRjw2DLQ== +"@tkey-mpc/share-serialization@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/share-serialization/-/share-serialization-8.2.2.tgz#b0d0aed02aad6082957af7d9b302c00f61a71335" + integrity sha512-pqaaKYHF6kKAPC72X+xlptLBwiUfr/BW3v1UiJQOCviCa4GThPkAyrAX80yPjKSeabWQxt3eoIc9m3e+Ydt4bw== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" "@types/create-hash" "1.2.2" bn.js "^5.2.1" create-hash "^1.2.0" -"@tkey-mpc/storage-layer-torus@^8.2.0": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@tkey-mpc/storage-layer-torus/-/storage-layer-torus-8.2.0.tgz#67fe17f1186e8a0edd1bc0d271e3496a3fcdfa66" - integrity sha512-btx2668PTnsbLzDuPBLJwpJuB7rd6R5PjLixCosPOKjV3371Jp6CeeSnkQSjjeEnCRFThz9D66ttSjgwICdPPg== +"@tkey-mpc/storage-layer-torus@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/storage-layer-torus/-/storage-layer-torus-8.2.2.tgz#e01a8e1f53ffc360c2f3f4cb9cc82af1b7a9e33a" + integrity sha512-BCwNrijcwwY1Z7itF+ATiAPxSeJXrHlqa3kor5HfAGgrgZMG6UFCMWpgwTLfCqfjVqT1C6yjC1aG7L5TXLT/Rg== dependencies: - "@tkey-mpc/common-types" "^8.2.0" + "@tkey-mpc/common-types" "^8.2.2" "@toruslabs/http-helpers" "^4.0.0" bn.js "^5.2.1" json-stable-stringify "^1.0.2" @@ -5726,18 +5726,18 @@ loglevel "^1.8.1" ts-custom-error "^3.3.1" -"@web3auth/mpc-core-kit@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.0.1.tgz#0a79165bcff1925d57463a10793a5323dd9141b2" - integrity sha512-TTr8bM+5SkcmGE94DdzxnmuTsO2QzTojlxF03Tj6KCWNDOIav+nj94m2oErxG8vB9JY6L5IfKc8/d+igP8F1Gg== - dependencies: - "@tkey-mpc/chrome-storage" "^8.2.0" - "@tkey-mpc/common-types" "^8.2.0" - "@tkey-mpc/core" "^8.2.0" - "@tkey-mpc/security-questions" "^8.2.0" - "@tkey-mpc/service-provider-torus" "^8.2.0" - "@tkey-mpc/share-serialization" "^8.2.0" - "@tkey-mpc/storage-layer-torus" "^8.2.0" +"@web3auth/mpc-core-kit@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.0.2.tgz#0d6ee19df4c30449d8e283532c1c4d8c8a034f5f" + integrity sha512-68qp8vwydpsZa9c7YCOSbFJ/HulgauSTMymJiNFQ0q/gssAK+rzUMi7GJq/1gWlY8DOozO9C+u5MJnt9XG3JDw== + dependencies: + "@tkey-mpc/chrome-storage" "^8.2.2" + "@tkey-mpc/common-types" "^8.2.2" + "@tkey-mpc/core" "^8.2.2" + "@tkey-mpc/security-questions" "^8.2.2" + "@tkey-mpc/service-provider-torus" "^8.2.2" + "@tkey-mpc/share-serialization" "^8.2.2" + "@tkey-mpc/storage-layer-torus" "^8.2.2" "@toruslabs/constants" "^13.0.1" "@toruslabs/customauth" "^16.0.4" "@toruslabs/eccrypto" "4.0.0" From dbd1cc1dbeb1b48ff2a18a6e6e64b142b6458da6 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 5 Oct 2023 09:47:39 +0200 Subject: [PATCH 05/52] feat: MFA through password / device factors(#2559) --- package.json | 1 + .../common/ConnectWallet/MPCWallet.tsx | 9 +- .../ConnectWallet/MPCWalletProvider.tsx | 2 +- .../common/ConnectWallet/PasswordRecovery.tsx | 64 ++++ .../create/steps/ConnectWalletStep/index.tsx | 44 +-- .../SignerAccountMFA/PasswordForm.tsx | 124 +++++++ .../settings/SignerAccountMFA/helper.ts | 68 ++++ .../settings/SignerAccountMFA/index.tsx | 21 ++ .../sidebar/SidebarNavigation/config.tsx | 4 + src/config/routes.ts | 1 + .../wallets/mpc/__tests__/useMPC.test.ts | 8 +- .../mpc/__tests__/useMPCWallet.test.ts | 317 ++++++++++++++++++ .../mpc/recovery/DeviceShareRecovery.ts | 44 +++ .../mpc/recovery/SecurityQuestionRecovery.ts | 48 +++ src/hooks/wallets/mpc/useMPC.ts | 6 +- src/hooks/wallets/mpc/useMPCWallet.ts | 82 +++-- src/pages/settings/signer-account.tsx | 37 ++ src/services/exceptions/ErrorCodes.ts | 1 + src/services/mpc/__tests__/module.test.ts | 4 +- src/services/mpc/module.ts | 6 +- 20 files changed, 825 insertions(+), 66 deletions(-) create mode 100644 src/components/common/ConnectWallet/PasswordRecovery.tsx create mode 100644 src/components/settings/SignerAccountMFA/PasswordForm.tsx create mode 100644 src/components/settings/SignerAccountMFA/helper.ts create mode 100644 src/components/settings/SignerAccountMFA/index.tsx create mode 100644 src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts create mode 100644 src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts create mode 100644 src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts create mode 100644 src/pages/settings/signer-account.tsx diff --git a/package.json b/package.json index ab8f5ec26a..f5f7b6a666 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.28.1", "@sentry/tracing": "^7.28.1", + "@tkey-mpc/common-types": "^8.2.2", "@truffle/hdwallet-provider": "^2.1.4", "@web3-onboard/coinbase": "^2.2.4", "@web3-onboard/core": "^2.21.0", diff --git a/src/components/common/ConnectWallet/MPCWallet.tsx b/src/components/common/ConnectWallet/MPCWallet.tsx index 3799144921..86b5e05ca6 100644 --- a/src/components/common/ConnectWallet/MPCWallet.tsx +++ b/src/components/common/ConnectWallet/MPCWallet.tsx @@ -1,9 +1,12 @@ +import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' import { Box, Button, CircularProgress } from '@mui/material' import { useContext } from 'react' import { MpcWalletContext } from './MPCWalletProvider' +import { PasswordRecovery } from './PasswordRecovery' export const MPCWallet = () => { - const { loginPending, triggerLogin, resetAccount, userInfo } = useContext(MpcWalletContext) + const { loginPending, triggerLogin, resetAccount, userInfo, walletState, recoverFactorWithPassword } = + useContext(MpcWalletContext) return ( <> @@ -28,6 +31,10 @@ export const MPCWallet = () => { )} )} + + {walletState === MPCWalletState.MANUAL_RECOVERY && ( + + )} ) } diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx index 30e2be7117..30360bdcb2 100644 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -6,7 +6,7 @@ type MPCWalletContext = { triggerLogin: () => Promise resetAccount: () => Promise upsertPasswordBackup: (password: string) => Promise - recoverFactorWithPassword: (password: string) => Promise + recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise walletState: MPCWalletState userInfo: { email: string | undefined diff --git a/src/components/common/ConnectWallet/PasswordRecovery.tsx b/src/components/common/ConnectWallet/PasswordRecovery.tsx new file mode 100644 index 0000000000..17c8fb6060 --- /dev/null +++ b/src/components/common/ConnectWallet/PasswordRecovery.tsx @@ -0,0 +1,64 @@ +import { VisibilityOff, Visibility } from '@mui/icons-material' +import { + DialogContent, + Typography, + TextField, + IconButton, + FormControlLabel, + Checkbox, + Button, + Box, +} from '@mui/material' +import { useState } from 'react' +import ModalDialog from '../ModalDialog' + +export const PasswordRecovery = ({ + recoverFactorWithPassword, +}: { + recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise +}) => { + const [showPassword, setShowPassword] = useState(false) + const [recoveryPassword, setRecoveryPassword] = useState('') + const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) + return ( + + + + + This browser is not registered with your Account yet. Please enter your recovery password to restore access + to this Account. + + + { + setRecoveryPassword(event.target.value) + }} + InputProps={{ + endAdornment: ( + setShowPassword((prev) => !prev)} + edge="end" + > + {showPassword ? : } + + ), + }} + /> + setStoreDeviceFactor((prev) => !prev)} />} + label="Do not ask again on this device" + /> + + + + + + + ) +} diff --git a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx index 8081ca8e8a..c242060b5d 100644 --- a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx +++ b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx @@ -1,8 +1,6 @@ import { useEffect, useState } from 'react' -import { Box, Button, Divider, Grid, Typography } from '@mui/material' +import { Box, Button, Divider, Typography } from '@mui/material' import useWallet from '@/hooks/wallets/useWallet' -import { useCurrentChain } from '@/hooks/useChains' -import { isPairingSupported } from '@/services/pairing/utils' import type { NewSafeFormData } from '@/components/new-safe/create' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' @@ -10,16 +8,12 @@ import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCre import layoutCss from '@/components/new-safe/create/styles.module.css' import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' -import PairingDescription from '@/components/common/PairingDetails/PairingDescription' -import PairingQRCode from '@/components/common/PairingDetails/PairingQRCode' import { usePendingSafe } from '../StatusStep/usePendingSafe' import { MPCWallet } from '@/components/common/ConnectWallet/MPCWallet' const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps) => { const [pendingSafe] = usePendingSafe() const wallet = useWallet() - const chain = useCurrentChain() - const isSupported = isPairingSupported(chain?.disabledWallets) const handleConnect = useConnectWallet() const [, setSubmitted] = useState(false) useSyncSafeCreationStep(setStep) @@ -37,33 +31,21 @@ const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps - - - - - + + + + - + - - or - + + or + - - - - {isSupported && ( - - - - Connect to {'Safe{Wallet}'} mobile - - - - )} - + + ) diff --git a/src/components/settings/SignerAccountMFA/PasswordForm.tsx b/src/components/settings/SignerAccountMFA/PasswordForm.tsx new file mode 100644 index 0000000000..0f4d905974 --- /dev/null +++ b/src/components/settings/SignerAccountMFA/PasswordForm.tsx @@ -0,0 +1,124 @@ +import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery' +import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' +import { Typography, TextField, FormControlLabel, Checkbox, Button, Box } from '@mui/material' +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import { useState, useMemo } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { enableMFA } from './helper' + +enum PasswordFieldNames { + oldPassword = 'oldPassword', + newPassword = 'newPassword', + confirmPassword = 'confirmPassword', + storeDeviceShare = 'storeDeviceShare', +} + +type PasswordFormData = { + [PasswordFieldNames.oldPassword]: string | undefined + [PasswordFieldNames.newPassword]: string + [PasswordFieldNames.confirmPassword]: string + [PasswordFieldNames.storeDeviceShare]: boolean +} + +export const PasswordForm = ({ mpcCoreKit }: { mpcCoreKit: Web3AuthMPCCoreKit }) => { + const formMethods = useForm({ + mode: 'all', + defaultValues: async () => { + const isDeviceShareStored = await new DeviceShareRecovery(mpcCoreKit).isEnabled() + return { + [PasswordFieldNames.confirmPassword]: '', + [PasswordFieldNames.oldPassword]: undefined, + [PasswordFieldNames.newPassword]: '', + [PasswordFieldNames.storeDeviceShare]: isDeviceShareStored, + } + }, + }) + + const { register, formState, getValues, control, handleSubmit } = formMethods + + const [enablingMFA, setEnablingMFA] = useState(false) + + const isPasswordSet = useMemo(() => { + const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) + return securityQuestions.isEnabled() + }, [mpcCoreKit]) + + const onSubmit = async (data: PasswordFormData) => { + setEnablingMFA(true) + try { + await enableMFA(mpcCoreKit, data) + } finally { + setEnablingMFA(false) + } + } + + return ( +
+ + {isPasswordSet ? ( + You already have a recovery password setup. + ) : ( + You have no password setup. We suggest adding one to secure your Account. + )} + {isPasswordSet && ( + + )} + + { + const currentNewPW = getValues(PasswordFieldNames.newPassword) + if (value !== currentNewPW) { + return 'Passwords do not match' + } + }, + })} + /> + + ( + } + label="Do not ask for second factor on this device" + /> + )} + /> + + + +
+ ) +} diff --git a/src/components/settings/SignerAccountMFA/helper.ts b/src/components/settings/SignerAccountMFA/helper.ts new file mode 100644 index 0000000000..9984df01cf --- /dev/null +++ b/src/components/settings/SignerAccountMFA/helper.ts @@ -0,0 +1,68 @@ +import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery' +import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' +import { logError } from '@/services/exceptions' +import ErrorCodes from '@/services/exceptions/ErrorCodes' +import { asError } from '@/services/exceptions/utils' +import { getPubKeyPoint } from '@tkey-mpc/common-types' +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import BN from 'bn.js' + +export const isMFAEnabled = (mpcCoreKit: Web3AuthMPCCoreKit) => { + if (!mpcCoreKit) { + return false + } + const { shareDescriptions } = mpcCoreKit.getKeyDetails() + return !Object.values(shareDescriptions).some((value) => value[0]?.includes('hashedShare')) +} + +export const enableMFA = async ( + mpcCoreKit: Web3AuthMPCCoreKit, + { + newPassword, + oldPassword, + storeDeviceShare, + }: { + newPassword: string + oldPassword: string | undefined + storeDeviceShare: boolean + }, +) => { + if (!mpcCoreKit) { + return + } + const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) + const deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) + try { + // 1. setup device factor with password recovery + await securityQuestions.upsertPassword(newPassword, oldPassword) + const securityQuestionFactor = await securityQuestions.recoverWithPassword(newPassword) + if (!securityQuestionFactor) { + throw Error('Could not recover using the new password recovery') + } + + if (!isMFAEnabled(mpcCoreKit)) { + // 2. enable MFA in mpcCoreKit + const recoveryFactor = await mpcCoreKit.enableMFA({}) + + // 3. remove the recovery factor the mpcCoreKit creates + const recoverKey = new BN(recoveryFactor, 'hex') + const recoverPubKey = getPubKeyPoint(recoverKey) + await mpcCoreKit.deleteFactor(recoverPubKey, recoverKey) + } + + const hasDeviceShare = await deviceShareRecovery.isEnabled() + + if (!hasDeviceShare && storeDeviceShare) { + await deviceShareRecovery.createAndStoreDeviceFactor() + } + + if (hasDeviceShare && !storeDeviceShare) { + // Switch to password recovery factor such that we can delete the device factor + await mpcCoreKit.inputFactorKey(new BN(securityQuestionFactor, 'hex')) + await deviceShareRecovery.removeDeviceFactor() + } + } catch (e) { + const error = asError(e) + logError(ErrorCodes._304, error.message) + } +} diff --git a/src/components/settings/SignerAccountMFA/index.tsx b/src/components/settings/SignerAccountMFA/index.tsx new file mode 100644 index 0000000000..b3918f7c65 --- /dev/null +++ b/src/components/settings/SignerAccountMFA/index.tsx @@ -0,0 +1,21 @@ +import useMPC from '@/hooks/wallets/mpc/useMPC' +import { Box, Typography } from '@mui/material' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' + +import { PasswordForm } from './PasswordForm' + +const SignerAccountMFA = () => { + const mpcCoreKit = useMPC() + + if (mpcCoreKit?.status !== COREKIT_STATUS.LOGGED_IN) { + return ( + + You are currently not logged in with a social account + + ) + } + + return +} + +export default SignerAccountMFA diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index dc7df0699c..b3c940981c 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -104,6 +104,10 @@ export const settingsNavItems = [ label: 'Environment variables', href: AppRoutes.settings.environmentVariables, }, + { + label: 'Signer account', + href: AppRoutes.settings.signerAccount, + }, ] export const generalSettingsNavItems = [ diff --git a/src/config/routes.ts b/src/config/routes.ts index 6bea1cd90d..ad24a9cf41 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -27,6 +27,7 @@ export const AppRoutes = { }, settings: { spendingLimits: '/settings/spending-limits', + signerAccount: '/settings/signer-account', setup: '/settings/setup', modules: '/settings/modules', index: '/settings', diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index 3f67c0a1e7..edf9dfe9c3 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -1,6 +1,6 @@ import * as useOnboard from '@/hooks/wallets/useOnboard' import { renderHook, waitFor } from '@/tests/test-utils' -import { getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' +import { _getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' import * as useChains from '@/hooks/useChains' import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' import { hexZeroPad } from 'ethers/lib/utils' @@ -104,7 +104,7 @@ describe('useInitMPC', () => { renderHook(() => useInitMPC()) await waitFor(() => { - expect(getMPCCoreKitInstance()).toBeDefined() + expect(_getMPCCoreKitInstance()).toBeDefined() expect(connectWalletSpy).not.toBeCalled() }) }) @@ -151,7 +151,7 @@ describe('useInitMPC', () => { await waitFor(() => { expect(connectWalletSpy).toBeCalled() - expect(getMPCCoreKitInstance()).toBeDefined() + expect(_getMPCCoreKitInstance()).toBeDefined() }) }) @@ -215,7 +215,7 @@ describe('useInitMPC', () => { await waitFor(() => { expect(mockChainChangedListener).toHaveBeenCalledWith('0x5') - expect(getMPCCoreKitInstance()).toBeDefined() + expect(_getMPCCoreKitInstance()).toBeDefined() expect(connectWalletSpy).not.toBeCalled() }) }) diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts new file mode 100644 index 0000000000..c6af1f84dd --- /dev/null +++ b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts @@ -0,0 +1,317 @@ +import { act, renderHook, waitFor } from '@/tests/test-utils' +import { MPCWalletState, useMPCWallet } from '../useMPCWallet' +import * as useOnboard from '@/hooks/wallets/useOnboard' +import { type OnboardAPI } from '@web3-onboard/core' +import { + COREKIT_STATUS, + type UserInfo, + type OauthLoginParams, + type Web3AuthMPCCoreKit, + type TssSecurityQuestion, +} from '@web3auth/mpc-core-kit' +import * as mpcCoreKit from '@web3auth/mpc-core-kit' +import { setMPCCoreKitInstance } from '../useMPC' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { ethers } from 'ethers' +import BN from 'bn.js' + +/** time until mock login resolves */ +const MOCK_LOGIN_TIME = 1000 + +/** + * Helper class for mocking MPC Core Kit login flow + */ +class MockMPCCoreKit { + status: COREKIT_STATUS = COREKIT_STATUS.INITIALIZED + state: { + userInfo: UserInfo | undefined + } = { + userInfo: undefined, + } + + private stateAfterLogin: COREKIT_STATUS + private userInfoAfterLogin: UserInfo | undefined + private expectedFactorKey: BN + /** + * + * @param stateAfterLogin State after loginWithOauth resolves + * @param userInfoAfterLogin User info to set in the state after loginWithOauth resolves + * @param expectedFactorKey For MFA login flow the expected factor key. If inputFactorKey gets called with the expected factor key the state switches to logged in + */ + constructor(stateAfterLogin: COREKIT_STATUS, userInfoAfterLogin: UserInfo, expectedFactorKey: BN = new BN(-1)) { + this.stateAfterLogin = stateAfterLogin + this.userInfoAfterLogin = userInfoAfterLogin + this.expectedFactorKey = expectedFactorKey + } + + loginWithOauth(params: OauthLoginParams): Promise { + return new Promise((resolve) => { + // Resolve after 1 sec + setTimeout(() => { + this.status = this.stateAfterLogin + this.state.userInfo = this.userInfoAfterLogin + resolve() + }, MOCK_LOGIN_TIME) + }) + } + + inputFactorKey(factorKey: BN) { + if (factorKey.eq(this.expectedFactorKey)) { + this.status = COREKIT_STATUS.LOGGED_IN + return Promise.resolve() + } else { + Promise.reject() + } + } +} + +describe('useMPCWallet', () => { + beforeAll(() => { + jest.useFakeTimers() + }) + beforeEach(() => { + jest.resetAllMocks() + setMPCCoreKitInstance(undefined) + }) + afterAll(() => { + jest.useRealTimers() + }) + it('should have state NOT_INITIALIZED initially', () => { + const { result } = renderHook(() => useMPCWallet()) + expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) + expect(result.current.userInfo.email).toBeUndefined() + }) + + describe('triggerLogin', () => { + it('should throw if Onboard is not initialized', () => { + const { result } = renderHook(() => useMPCWallet()) + expect(result.current.triggerLogin()).rejects.toEqual(new Error('Onboard is not initialized')) + expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) + }) + + it('should throw if MPC Core Kit is not initialized', () => { + jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) + const { result } = renderHook(() => useMPCWallet()) + + expect(result.current.triggerLogin()).rejects.toEqual(new Error('MPC Core Kit is not initialized')) + expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) + }) + + it('should handle successful log in for SFA account', async () => { + jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + setMPCCoreKitInstance( + new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit, + ) + const { result } = renderHook(() => useMPCWallet()) + + act(() => { + result.current.triggerLogin() + }) + + // While the login resolves we are in Authenticating state + expect(result.current.walletState === MPCWalletState.AUTHENTICATING) + expect(connectWalletSpy).not.toBeCalled() + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // We should be logged in and onboard should get connected + await waitFor(() => { + expect(result.current.walletState === MPCWalletState.READY) + expect(connectWalletSpy).toBeCalledWith(expect.anything(), { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }) + }) + }) + + it('should handle successful log in for MFA account with device share', async () => { + const mockDeviceFactor = ethers.Wallet.createRandom().privateKey.slice(2) + jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + setMPCCoreKitInstance( + new MockMPCCoreKit( + COREKIT_STATUS.REQUIRED_SHARE, + { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo, + new BN(mockDeviceFactor, 'hex'), + ) as unknown as Web3AuthMPCCoreKit, + ) + + jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(mockDeviceFactor)) + + const { result } = renderHook(() => useMPCWallet()) + + act(() => { + result.current.triggerLogin() + }) + + // While the login resolves we are in Authenticating state + expect(result.current.walletState === MPCWalletState.AUTHENTICATING) + expect(connectWalletSpy).not.toBeCalled() + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // We should be logged in and onboard should get connected + await waitFor(() => { + expect(result.current.walletState === MPCWalletState.READY) + expect(connectWalletSpy).toBeCalledWith(expect.anything(), { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }) + }) + }) + + it('should require manual share for MFA account without device share', async () => { + jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + setMPCCoreKitInstance( + new MockMPCCoreKit(COREKIT_STATUS.REQUIRED_SHARE, { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit, + ) + + // TODO: remove unnecessary cast if mpc core sdk gets updated + jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(undefined as unknown as string)) + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + } as unknown as TssSecurityQuestion) + + const { result } = renderHook(() => useMPCWallet()) + + act(() => { + result.current.triggerLogin() + }) + + // While the login resolves we are in Authenticating state + expect(result.current.walletState === MPCWalletState.AUTHENTICATING) + expect(connectWalletSpy).not.toBeCalled() + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // A missing second factor should result in manual recovery state + await waitFor(() => { + expect(result.current.walletState === MPCWalletState.MANUAL_RECOVERY) + expect(connectWalletSpy).not.toBeCalled() + }) + }) + }) + + describe('resetAccount', () => { + it('should throw if mpcCoreKit is not initialized', () => { + const { result } = renderHook(() => useMPCWallet()) + expect(result.current.resetAccount()).rejects.toEqual( + new Error('MPC Core Kit is not initialized or the user is not logged in'), + ) + }) + it('should reset an account by overwriting the metadata', async () => { + const mockSetMetadata = jest.fn() + const mockMPCCore = { + metadataKey: ethers.Wallet.createRandom().privateKey.slice(2), + state: { + userInfo: undefined, + }, + tKey: { + storageLayer: { + setMetadata: mockSetMetadata, + }, + }, + } + + setMPCCoreKitInstance(mockMPCCore as unknown as Web3AuthMPCCoreKit) + + const { result } = renderHook(() => useMPCWallet()) + + await result.current.resetAccount() + + expect(mockSetMetadata).toHaveBeenCalledWith({ + privKey: new BN(mockMPCCore.metadataKey, 'hex'), + input: { message: 'KEY_NOT_FOUND' }, + }) + }) + }) + + describe('recoverFactorWithPassword', () => { + it('should throw if mpcCoreKit is not initialized', () => { + const { result } = renderHook(() => useMPCWallet()) + expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual( + new Error('MPC Core Kit is not initialized'), + ) + }) + + it('should not recover if wrong password is entered', () => { + setMPCCoreKitInstance({ + state: { + userInfo: undefined, + }, + } as unknown as Web3AuthMPCCoreKit) + const { result } = renderHook(() => useMPCWallet()) + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + recoverFactor: () => { + throw new Error('Invalid answer') + }, + } as unknown as TssSecurityQuestion) + + expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual(new Error('Invalid answer')) + }) + + it('should input recovered factor if correct password is entered', async () => { + const mockSecurityQuestionFactor = ethers.Wallet.createRandom().privateKey.slice(2) + const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) + jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + + setMPCCoreKitInstance( + new MockMPCCoreKit( + COREKIT_STATUS.REQUIRED_SHARE, + { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo, + new BN(mockSecurityQuestionFactor, 'hex'), + ) as unknown as Web3AuthMPCCoreKit, + ) + + const { result } = renderHook(() => useMPCWallet()) + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + recoverFactor: () => Promise.resolve(mockSecurityQuestionFactor), + } as unknown as TssSecurityQuestion) + + act(() => result.current.recoverFactorWithPassword('test', false)) + + await waitFor(() => { + expect(result.current.walletState === MPCWalletState.READY) + expect(connectWalletSpy).toBeCalledWith(expect.anything(), { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }) + }) + }) + }) +}) diff --git a/src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts b/src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts new file mode 100644 index 0000000000..8a080ec626 --- /dev/null +++ b/src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts @@ -0,0 +1,44 @@ +import { + BrowserStorage, + getWebBrowserFactor, + storeWebBrowserFactor, + TssShareType, + type Web3AuthMPCCoreKit, +} from '@web3auth/mpc-core-kit' +import BN from 'bn.js' +import { getPubKeyPoint } from '@tkey-mpc/common-types' + +export class DeviceShareRecovery { + private mpcCoreKit: Web3AuthMPCCoreKit + + constructor(mpcCoreKit: Web3AuthMPCCoreKit) { + this.mpcCoreKit = mpcCoreKit + } + + async isEnabled() { + if (!this.mpcCoreKit.tKey.metadata) { + return false + } + return !!(await getWebBrowserFactor(this.mpcCoreKit)) + } + + async createAndStoreDeviceFactor() { + const userAgent = navigator.userAgent + + const deviceFactorKey = new BN( + await this.mpcCoreKit.createFactor({ shareType: TssShareType.DEVICE, additionalMetadata: { userAgent } }), + 'hex', + ) + await storeWebBrowserFactor(deviceFactorKey, this.mpcCoreKit) + } + + async removeDeviceFactor() { + const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit) + const key = new BN(deviceFactor, 'hex') + const pubKey = getPubKeyPoint(key) + const pubKeyX = pubKey.x.toString('hex', 64) + await this.mpcCoreKit.deleteFactor(pubKey) + const currentStorage = BrowserStorage.getInstance('mpc_corekit_store') + currentStorage.set(pubKeyX, undefined) + } +} diff --git a/src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts b/src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts new file mode 100644 index 0000000000..0d707cb29f --- /dev/null +++ b/src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts @@ -0,0 +1,48 @@ +import { TssSecurityQuestion, TssShareType, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' + +export class SecurityQuestionRecovery { + /** This is only used internally in the metadata store of tKey. Not in the UI */ + private static readonly DEFAULT_SECURITY_QUESTION = 'ENTER PASSWORD' + + private mpcCoreKit: Web3AuthMPCCoreKit + private securityQuestions = new TssSecurityQuestion() + + constructor(mpcCoreKit: Web3AuthMPCCoreKit) { + this.mpcCoreKit = mpcCoreKit + } + + isEnabled(): boolean { + try { + const question = this.securityQuestions.getQuestion(this.mpcCoreKit) + return !!question + } catch (error) { + // It errors out if recovery is not setup currently + return false + } + } + + async upsertPassword(newPassword: string, oldPassword?: string) { + if (this.isEnabled()) { + if (!oldPassword) { + throw Error('To change the password you need to provide the old password') + } + await this.securityQuestions.changeSecurityQuestion({ + answer: oldPassword, + mpcCoreKit: this.mpcCoreKit, + newAnswer: newPassword, + newQuestion: SecurityQuestionRecovery.DEFAULT_SECURITY_QUESTION, + }) + } else { + await this.securityQuestions.setSecurityQuestion({ + question: SecurityQuestionRecovery.DEFAULT_SECURITY_QUESTION, + answer: newPassword, + mpcCoreKit: this.mpcCoreKit, + shareType: TssShareType.DEVICE, + }) + } + } + + async recoverWithPassword(password: string) { + return this.securityQuestions.recoverFactor(this.mpcCoreKit, password) + } +} diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index ba110255ea..c7f4607c05 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -24,7 +24,7 @@ export const useInitMPC = () => { chainNamespace: CHAIN_NAMESPACES.EIP155, rpcTarget: getRpcServiceUrl(chain.rpcUri), displayName: chain.chainName, - blockExplorer: chain.blockExplorerUriTemplate.address, + blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, ticker: chain.nativeCurrency.symbol, tickerName: chain.nativeCurrency.name, } @@ -40,7 +40,7 @@ export const useInitMPC = () => { const web3AuthCoreKit = new Web3AuthMPCCoreKit({ web3AuthClientId: WEB3_AUTH_CLIENT_ID, // Available networks are "sapphire_devnet", "sapphire_mainnet" - web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, + web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, baseUrl: `${window.location.origin}/serviceworker`, uxMode: 'popup', enableLogging: true, @@ -79,7 +79,7 @@ export const useInitMPC = () => { }, [chain, onboard]) } -export const getMPCCoreKitInstance = getStore +export const _getMPCCoreKitInstance = getStore export const setMPCCoreKitInstance = setStore diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index f0d54044c5..9fc69b4d06 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -2,24 +2,22 @@ import { useState } from 'react' import useMPC from './useMPC' import BN from 'bn.js' import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS, getWebBrowserFactor } from '@web3auth/mpc-core-kit' import useOnboard, { connectWallet } from '../useOnboard' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { SecurityQuestionRecovery } from './recovery/SecurityQuestionRecovery' +import { DeviceShareRecovery } from './recovery/DeviceShareRecovery' export enum MPCWalletState { NOT_INITIALIZED, AUTHENTICATING, - AUTHENTICATED, - CREATING_SECOND_FACTOR, - RECOVERING_ACCOUNT_PASSWORD, - CREATED_SECOND_FACTOR, - FINALIZING_ACCOUNT, + MANUAL_RECOVERY, READY, } export type MPCWalletHook = { upsertPasswordBackup: (password: string) => Promise - recoverFactorWithPassword: (password: string) => Promise + recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise walletState: MPCWalletState triggerLogin: () => Promise resetAccount: () => Promise @@ -38,7 +36,7 @@ export const useMPCWallet = (): MPCWalletHook => { // Resetting your account means clearing all the metadata associated with it from the metadata server // The key details will be deleted from our server and you will not be able to recover your account if (!mpcCoreKit || !mpcCoreKit.metadataKey) { - throw new Error('coreKitInstance is not set or the user is not logged in') + throw new Error('MPC Core Kit is not initialized or the user is not logged in') } // In web3auth an account is reset by overwriting the metadata with KEY_NOT_FOUND @@ -50,13 +48,11 @@ export const useMPCWallet = (): MPCWalletHook => { const triggerLogin = async () => { if (!onboard) { - console.error('Onboard not initialized') - return + throw Error('Onboard is not initialized') } if (!mpcCoreKit) { - console.error('tKey not initialized yet') - return + throw Error('MPC Core Kit is not initialized') } try { setWalletState(MPCWalletState.AUTHENTICATING) @@ -68,26 +64,70 @@ export const useMPCWallet = (): MPCWalletHook => { }, }) - if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + if (mpcCoreKit.status === COREKIT_STATUS.REQUIRED_SHARE) { + // Check if we have a device share stored + const deviceFactor = await getWebBrowserFactor(mpcCoreKit) + if (deviceFactor) { + // Recover from device factor + const deviceFactorKey = new BN(deviceFactor, 'hex') + await mpcCoreKit.inputFactorKey(deviceFactorKey) + } else { + // Check password recovery + const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) + if (securityQuestions.isEnabled()) { + setWalletState(MPCWalletState.MANUAL_RECOVERY) + return + } + } } - setWalletState(MPCWalletState.AUTHENTICATED) + finalizeLogin() } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) } } + const finalizeLogin = () => { + if (!mpcCoreKit || !onboard) { + return + } + if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + setWalletState(MPCWalletState.READY) + } + } + + const recoverFactorWithPassword = async (password: string, storeDeviceShare: boolean = false) => { + if (!mpcCoreKit) { + throw new Error('MPC Core Kit is not initialized') + } + + const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) + + if (securityQuestions.isEnabled()) { + const factorKeyString = await securityQuestions.recoverWithPassword(password) + const factorKey = new BN(factorKeyString, 'hex') + await mpcCoreKit.inputFactorKey(factorKey) + + if (storeDeviceShare) { + const deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) + await deviceShareRecovery.createAndStoreDeviceFactor() + } + + finalizeLogin() + } + } + return { triggerLogin, walletState, - recoverFactorWithPassword: () => Promise.resolve(), + recoverFactorWithPassword, resetAccount: criticalResetAccount, upsertPasswordBackup: () => Promise.resolve(), userInfo: { diff --git a/src/pages/settings/signer-account.tsx b/src/pages/settings/signer-account.tsx new file mode 100644 index 0000000000..3c0f36b91b --- /dev/null +++ b/src/pages/settings/signer-account.tsx @@ -0,0 +1,37 @@ +import { Grid, Paper, Typography } from '@mui/material' + +import type { NextPage } from 'next' +import Head from 'next/head' + +import SettingsHeader from '@/components/settings/SettingsHeader' +import SignerAccountMFA from '@/components/settings/SignerAccountMFA' + +const SignerAccountPage: NextPage = () => { + return ( + <> + + {'Safe{Wallet} – Settings – Signer account'} + + + + +
+ + + + + Multi-factor Authentication + + + + + + + + +
+ + ) +} + +export default SignerAccountPage diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 22fe117513..f9a6986dc7 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -16,6 +16,7 @@ enum ErrorCodes { _302 = '302: Error connecting to the wallet', _303 = '303: Error creating pairing session', + _304 = '304: Error enabling MFA', _600 = '600: Error fetching Safe info', _601 = '601: Error fetching balances', diff --git a/src/services/mpc/__tests__/module.test.ts b/src/services/mpc/__tests__/module.test.ts index 4a510b992f..3a03f09c81 100644 --- a/src/services/mpc/__tests__/module.test.ts +++ b/src/services/mpc/__tests__/module.test.ts @@ -36,7 +36,7 @@ describe('MPC Onboard module', () => { send: mockReadOnlySend, } as any) - jest.spyOn(useMPC, 'getMPCCoreKitInstance').mockImplementation(() => { + jest.spyOn(useMPC, '_getMPCCoreKitInstance').mockImplementation(() => { return { provider: {}, } as any @@ -85,7 +85,7 @@ describe('MPC Onboard module', () => { send: mockReadOnlySend, } as any) - jest.spyOn(useMPC, 'getMPCCoreKitInstance').mockImplementation(() => { + jest.spyOn(useMPC, '_getMPCCoreKitInstance').mockImplementation(() => { return { provider: { request: mockMPCProviderRequest, diff --git a/src/services/mpc/module.ts b/src/services/mpc/module.ts index 180cbddc43..1814f111d0 100644 --- a/src/services/mpc/module.ts +++ b/src/services/mpc/module.ts @@ -1,9 +1,9 @@ -import { getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' +import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' import { type EIP1193Provider } from '@web3-onboard/core' -const getMPCProvider = () => getMPCCoreKitInstance()?.provider +const getMPCProvider = () => _getMPCCoreKitInstance()?.provider const assertDefined = (mpcProvider: T | undefined) => { if (!mpcProvider) { @@ -78,7 +78,7 @@ function MpcModule(): WalletInit { return web3.removeListener(event, listener) }, disconnect: () => { - getMPCCoreKitInstance()?.logout() + _getMPCCoreKitInstance()?.logout() }, } From cf8db3f1c1f849708b23b905450150b51a797bbc Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 6 Oct 2023 13:49:41 +0200 Subject: [PATCH 06/52] feat: redesign welcome page (#2593) --- public/images/common/check-filled.svg | 3 + public/images/logo-text.svg | 9 + public/images/welcome/logo-google.svg | 6 + .../common/ConnectWallet/AccountCenter.tsx | 3 - .../common/ConnectWallet/MPCLogin.tsx | 82 ++++++++ .../common/ConnectWallet/MPCWallet.tsx | 40 ---- .../ConnectWallet/MPCWalletProvider.tsx | 9 +- .../common/ConnectWallet/WalletDetails.tsx | 4 +- .../ConnectWallet/__tests__/MPCLogin.test.tsx | 107 ++++++++++ .../common/ConnectWallet/styles.module.css | 12 ++ .../__tests__/useSyncSafeCreationStep.test.ts | 14 +- src/components/new-safe/create/index.tsx | 8 - .../create/steps/ConnectWalletStep/index.tsx | 54 ----- .../create/useSyncSafeCreationStep.ts | 12 +- src/components/welcome/NewSafe.tsx | 189 ++++++------------ .../welcome/WelcomeLogin/WalletLogin.tsx | 56 ++++++ .../__tests__/WalletLogin.test.tsx | 71 +++++++ src/components/welcome/WelcomeLogin/index.tsx | 53 +++++ .../welcome/WelcomeLogin/styles.module.css | 18 ++ src/components/welcome/styles.module.css | 12 +- .../mpc/__tests__/useMPCWallet.test.ts | 2 +- src/hooks/wallets/mpc/useMPCWallet.ts | 18 +- 22 files changed, 514 insertions(+), 268 deletions(-) create mode 100644 public/images/common/check-filled.svg create mode 100644 public/images/logo-text.svg create mode 100644 public/images/welcome/logo-google.svg create mode 100644 src/components/common/ConnectWallet/MPCLogin.tsx delete mode 100644 src/components/common/ConnectWallet/MPCWallet.tsx create mode 100644 src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx delete mode 100644 src/components/new-safe/create/steps/ConnectWalletStep/index.tsx create mode 100644 src/components/welcome/WelcomeLogin/WalletLogin.tsx create mode 100644 src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx create mode 100644 src/components/welcome/WelcomeLogin/index.tsx create mode 100644 src/components/welcome/WelcomeLogin/styles.module.css diff --git a/public/images/common/check-filled.svg b/public/images/common/check-filled.svg new file mode 100644 index 0000000000..284624fc19 --- /dev/null +++ b/public/images/common/check-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/logo-text.svg b/public/images/logo-text.svg new file mode 100644 index 0000000000..b71ae734a4 --- /dev/null +++ b/public/images/logo-text.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/welcome/logo-google.svg b/public/images/welcome/logo-google.svg new file mode 100644 index 0000000000..65781d4881 --- /dev/null +++ b/public/images/welcome/logo-google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 082a0fab54..03a0aab974 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -13,7 +13,6 @@ import ChainSwitcher from '../ChainSwitcher' import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' -import { MPCWallet } from './MPCWallet' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) @@ -115,8 +114,6 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { - - diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx new file mode 100644 index 0000000000..e55a32647b --- /dev/null +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -0,0 +1,82 @@ +import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' +import { Box, Button, SvgIcon, Typography } from '@mui/material' +import { useContext, useEffect, useState } from 'react' +import { MpcWalletContext } from './MPCWalletProvider' +import { PasswordRecovery } from './PasswordRecovery' +import GoogleLogo from '@/public/images/welcome/logo-google.svg' + +import css from './styles.module.css' +import useWallet from '@/hooks/wallets/useWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' + +const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { + const { loginPending, triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) + + const wallet = useWallet() + + const [loginTriggered, setLoginTriggered] = useState(false) + + const login = async () => { + setLoginTriggered(true) + await triggerLogin() + } + + // If login was triggered through the Button we immediately continue if logged in + useEffect(() => { + if (loginTriggered && wallet && wallet.label === ONBOARD_MPC_MODULE_LABEL && onLogin) { + onLogin() + } + }, [loginTriggered, onLogin, wallet]) + + return ( + <> + {wallet && userInfo ? ( + <> + + + ) : ( + + )} + + {walletState === MPCWalletState.MANUAL_RECOVERY && ( + + )} + + ) +} + +export default MPCLogin diff --git a/src/components/common/ConnectWallet/MPCWallet.tsx b/src/components/common/ConnectWallet/MPCWallet.tsx deleted file mode 100644 index 86b5e05ca6..0000000000 --- a/src/components/common/ConnectWallet/MPCWallet.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' -import { Box, Button, CircularProgress } from '@mui/material' -import { useContext } from 'react' -import { MpcWalletContext } from './MPCWalletProvider' -import { PasswordRecovery } from './PasswordRecovery' - -export const MPCWallet = () => { - const { loginPending, triggerLogin, resetAccount, userInfo, walletState, recoverFactorWithPassword } = - useContext(MpcWalletContext) - - return ( - <> - {userInfo.email ? ( - <> - - - - - - - ) : ( - - )} - - {walletState === MPCWalletState.MANUAL_RECOVERY && ( - - )} - - ) -} diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx index 30360bdcb2..4f91450e26 100644 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -1,4 +1,5 @@ import { useMPCWallet, MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' +import { type UserInfo } from '@web3auth/mpc-core-kit' import { createContext, type ReactElement } from 'react' type MPCWalletContext = { @@ -8,9 +9,7 @@ type MPCWalletContext = { upsertPasswordBackup: (password: string) => Promise recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise walletState: MPCWalletState - userInfo: { - email: string | undefined - } + userInfo: UserInfo | undefined } export const MpcWalletContext = createContext({ @@ -20,9 +19,7 @@ export const MpcWalletContext = createContext({ resetAccount: () => Promise.resolve(), upsertPasswordBackup: () => Promise.resolve(), recoverFactorWithPassword: () => Promise.resolve(), - userInfo: { - email: undefined, - }, + userInfo: undefined, }) export const MpcWalletProvider = ({ children }: { children: ReactElement }) => { diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index c4fb5f8c2d..1104d9dd23 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -3,7 +3,7 @@ import type { ReactElement } from 'react' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' -import { MPCWallet } from './MPCWallet' +import MPCLogin from './MPCLogin' const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement => { const connectWallet = useConnectWallet() @@ -23,7 +23,7 @@ const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement Connect - + ) } diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx new file mode 100644 index 0000000000..41952da2b2 --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -0,0 +1,107 @@ +import { act, render, waitFor } from '@/tests/test-utils' +import * as useWallet from '@/hooks/wallets/useWallet' +import * as useMPCWallet from '@/hooks/wallets/mpc/useMPCWallet' +import MPCLogin from '../MPCLogin' +import { hexZeroPad } from '@ethersproject/bytes' +import { type EIP1193Provider } from '@web3-onboard/common' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { MpcWalletProvider } from '../MPCWalletProvider' + +describe('MPCLogin', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render continue with connected account', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + jest.spyOn(useWallet, 'default').mockReturnValue({ + address: walletAddress, + chainId: '5', + label: ONBOARD_MPC_MODULE_LABEL, + provider: {} as unknown as EIP1193Provider, + }) + jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ + userInfo: { + email: 'test@safe.test', + name: 'Test Testermann', + profileImage: 'test.png', + }, + triggerLogin: jest.fn(), + walletState: useMPCWallet.MPCWalletState.READY, + } as unknown as useMPCWallet.MPCWalletHook) + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() + }) + + it('should render google login button and invoke the callback on connection if no wallet is connected', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) + const mockTriggerLogin = jest.fn() + const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ + userInfo: { + email: undefined, + name: undefined, + profileImage: undefined, + }, + triggerLogin: mockTriggerLogin, + walletState: useMPCWallet.MPCWalletState.NOT_INITIALIZED, + } as unknown as useMPCWallet.MPCWalletHook) + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue with Google')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + await act(async () => { + // Click the button and mock a successful login + const button = await result.findByRole('button') + button.click() + mockUseMPCWallet.mockReset().mockReturnValue({ + userInfo: { + email: 'test@safe.test', + name: 'Test Testermann', + profileImage: 'test.png', + }, + triggerLogin: jest.fn(), + walletState: useMPCWallet.MPCWalletState.READY, + } as unknown as useMPCWallet.MPCWalletHook) + + mockUseWallet.mockReset().mockReturnValue({ + address: walletAddress, + chainId: '5', + label: ONBOARD_MPC_MODULE_LABEL, + provider: {} as unknown as EIP1193Provider, + }) + }) + + await waitFor(() => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index b189d44bee..aa127889c6 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -28,6 +28,18 @@ width: 100%; } +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; +} + .rowContainer { align-self: stretch; margin-left: calc(var(--space-2) * -1); diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index 807f848364..a3bc08f3da 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -5,6 +5,9 @@ import * as localStorage from '@/services/local-storage/useLocalStorage' import type { ConnectedWallet } from '@/services/onboard' import * as usePendingSafe from '../steps/StatusStep/usePendingSafe' import * as useIsWrongChain from '@/hooks/useIsWrongChain' +import * as useRouter from 'next/router' +import { type NextRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' describe('useSyncSafeCreationStep', () => { const mockPendingSafe = { @@ -22,13 +25,18 @@ describe('useSyncSafeCreationStep', () => { }) it('should go to the first step if no wallet is connected', async () => { + const mockPushRoute = jest.fn() jest.spyOn(wallet, 'default').mockReturnValue(null) jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) + jest.spyOn(useRouter, 'useRouter').mockReturnValue({ + push: mockPushRoute, + } as unknown as NextRouter) const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(0) + expect(mockSetStep).not.toHaveBeenCalled() + expect(mockPushRoute).toHaveBeenCalledWith(AppRoutes.welcome) }) it('should go to the fourth step if there is a pending safe', async () => { @@ -40,7 +48,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(4) + expect(mockSetStep).toHaveBeenCalledWith(3) }) it('should go to the second step if the wrong chain is connected', async () => { @@ -53,7 +61,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(1) + expect(mockSetStep).toHaveBeenCalledWith(0) }) it('should not do anything if wallet is connected and there is no pending safe', async () => { diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index a0807aa59a..f2b179d593 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -5,7 +5,6 @@ import useWallet from '@/hooks/wallets/useWallet' import OverviewWidget from '@/components/new-safe/create/OverviewWidget' import type { NamedAddress } from '@/components/new-safe/create/types' import type { TxStepperProps } from '@/components/new-safe/CardStepper/useCardStepper' -import ConnectWalletStep from '@/components/new-safe/create/steps/ConnectWalletStep' import SetNameStep from '@/components/new-safe/create/steps/SetNameStep' import OwnerPolicyStep from '@/components/new-safe/create/steps/OwnerPolicyStep' import ReviewStep from '@/components/new-safe/create/steps/ReviewStep' @@ -111,13 +110,6 @@ const CreateSafe = () => { const [activeStep, setActiveStep] = useState(0) const CreateSafeSteps: TxStepperProps['steps'] = [ - { - title: 'Connect wallet', - subtitle: 'The connected wallet will pay the network fees for the Safe Account creation.', - render: (data, onSubmit, onBack, setStep) => ( - - ), - }, { title: 'Select network and name of your Safe Account', subtitle: 'Select the network on which to create your Safe Account', diff --git a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx deleted file mode 100644 index c242060b5d..0000000000 --- a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useState } from 'react' -import { Box, Button, Divider, Typography } from '@mui/material' -import useWallet from '@/hooks/wallets/useWallet' - -import type { NewSafeFormData } from '@/components/new-safe/create' -import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' -import layoutCss from '@/components/new-safe/create/styles.module.css' -import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' -import KeyholeIcon from '@/components/common/icons/KeyholeIcon' -import { usePendingSafe } from '../StatusStep/usePendingSafe' -import { MPCWallet } from '@/components/common/ConnectWallet/MPCWallet' - -const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps) => { - const [pendingSafe] = usePendingSafe() - const wallet = useWallet() - const handleConnect = useConnectWallet() - const [, setSubmitted] = useState(false) - useSyncSafeCreationStep(setStep) - - useEffect(() => { - if (!wallet || pendingSafe) return - - setSubmitted((prev) => { - if (prev) return prev - onSubmit({ owners: [{ address: wallet.address, name: wallet.ens || '' }] }) - return true - }) - }, [onSubmit, wallet, pendingSafe]) - - return ( - <> - - - - - - - - - - or - - - - - - - ) -} - -export default ConnectWalletStep diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 6b4ad6ff94..3033daf932 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -4,31 +4,33 @@ import type { NewSafeFormData } from '@/components/new-safe/create/index' import useWallet from '@/hooks/wallets/useWallet' import { usePendingSafe } from './steps/StatusStep/usePendingSafe' import useIsWrongChain from '@/hooks/useIsWrongChain' +import { useRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' const useSyncSafeCreationStep = (setStep: StepRenderProps['setStep']) => { const [pendingSafe] = usePendingSafe() const wallet = useWallet() const isWrongChain = useIsWrongChain() + const router = useRouter() useEffect(() => { // Jump to the status screen if there is already a tx submitted if (pendingSafe) { - setStep(4) + setStep(3) return } // Jump to connect wallet step if there is no wallet and no pending Safe if (!wallet) { - setStep(0) - return + router.push(AppRoutes.welcome) } // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe if (isWrongChain) { - setStep(1) + setStep(0) return } - }, [wallet, setStep, pendingSafe, isWrongChain]) + }, [wallet, setStep, pendingSafe, isWrongChain, router]) } export default useSyncSafeCreationStep diff --git a/src/components/welcome/NewSafe.tsx b/src/components/welcome/NewSafe.tsx index 9387296884..02994badc6 100644 --- a/src/components/welcome/NewSafe.tsx +++ b/src/components/welcome/NewSafe.tsx @@ -1,156 +1,79 @@ -import React, { useEffect, useState } from 'react' -import { - Button, - Grid, - Paper, - SvgIcon, - Typography, - AccordionSummary, - AccordionDetails, - Accordion, - useMediaQuery, - Box, -} from '@mui/material' -import { useRouter } from 'next/router' -import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' -import Track from '../common/Track' -import { AppRoutes } from '@/config/routes' +import React, { useState } from 'react' +import { Accordion, AccordionDetails, AccordionSummary, Box, Grid, SvgIcon, Typography } from '@mui/material' import SafeList from '@/components/sidebar/SafeList' import css from './styles.module.css' -import NewSafeIcon from '@/public/images/welcome/new-safe.svg' -import LoadSafeIcon from '@/public/images/welcome/load-safe.svg' +import CheckFilled from '@/public/images/common/check-filled.svg' + import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { useTheme } from '@mui/material/styles' import { DataWidget } from '@/components/welcome/DataWidget' -import useWallet from '@/hooks/wallets/useWallet' -import { useCurrentChain } from '@/hooks/useChains' +import WelcomeLogin from './WelcomeLogin' +import { useAppSelector } from '@/store' +import { selectTotalAdded } from '@/store/addedSafesSlice' const NewSafe = () => { - const [expanded, setExpanded] = useState(true) - const router = useRouter() - const theme = useTheme() - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')) - const wallet = useWallet() - const currentChain = useCurrentChain() + const [expanded, setExpanded] = useState(false) - useEffect(() => { - setExpanded(!isSmallScreen) - }, [isSmallScreen]) + const addedSafes = useAppSelector(selectTotalAdded) const toggleSafeList = () => { - return isSmallScreen ? setExpanded((prev) => !prev) : null + return setExpanded((prev) => !prev) } return ( - - - - - - }> - - My Safe Accounts - - - - event.stopPropagation()}> - - - - - - - - - + + + - - - - - - - +
- - Welcome to {'Safe{Wallet}'} + {addedSafes > 0 && ( + + + + + }> + + My Safe Accounts ({addedSafes}) + + + + event.stopPropagation()}> + + + + + + + + + + )} + + Unlock a new way of ownership - + The most trusted decentralized custody protocol and collective asset management platform. - - - - - - Create Safe Account - - - - A new Account that is controlled by one or multiple owners. - - - - - - - + + + + + Stealth security with multiple owners + - - - - - - Add existing Account - - - - Already have a Safe Account? Add it via its address. - - - - - - - + + + + Make it yours with modules and guards + + + + + + Access 130+ ecosystem apps +
diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx new file mode 100644 index 0000000000..758a3b23c7 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -0,0 +1,56 @@ +import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import useWallet from '@/hooks/wallets/useWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { Box, Button, Typography } from '@mui/material' +import { EthHashInfo } from '@safe-global/safe-react-components' +import { useState, useEffect } from 'react' + +const WalletLogin = ({ onLogin }: { onLogin?: () => void }) => { + const wallet = useWallet() + const connectWallet = useConnectWallet() + + const [loginTriggered, setLoginTriggered] = useState(false) + + const login = async () => { + setLoginTriggered(true) + await connectWallet() + } + + // If login was triggered through the Button we immediately continue if logged in + useEffect(() => { + if (loginTriggered && wallet && onLogin) { + onLogin() + } + }, [loginTriggered, onLogin, wallet]) + + if (wallet !== null && wallet?.label !== ONBOARD_MPC_MODULE_LABEL) { + return ( + + ) + } + + return ( + + ) +} + +export default WalletLogin diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx new file mode 100644 index 0000000000..d3dcf4d715 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -0,0 +1,71 @@ +import { act, render, waitFor } from '@/tests/test-utils' +import * as useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import * as useWallet from '@/hooks/wallets/useWallet' +import WalletLogin from '../WalletLogin' +import { hexZeroPad } from '@ethersproject/bytes' +import { type EIP1193Provider } from '@web3-onboard/common' +import { shortenAddress } from '@/utils/formatters' + +describe('WalletLogin', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render continue with connected wallet', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + jest.spyOn(useWallet, 'default').mockReturnValue({ + address: walletAddress, + chainId: '5', + label: 'MetaMask', + provider: {} as unknown as EIP1193Provider, + }) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn()) + + const result = render() + + await waitFor(() => { + expect(result.findByText(shortenAddress(walletAddress))).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() + }) + + it('should render connect wallet and invoke the callback on connection if no wallet is connected', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn()) + + const result = render() + + await waitFor(() => { + expect(result.findByText('Connect wallet')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + await act(async () => { + // Click the button and mock a wallet connection + const button = await result.findByRole('button') + button.click() + mockUseWallet.mockReset().mockReturnValue({ + address: walletAddress, + chainId: '5', + label: 'MetaMask', + provider: {} as unknown as EIP1193Provider, + }) + }) + + await waitFor(() => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx new file mode 100644 index 0000000000..4d08ea716a --- /dev/null +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -0,0 +1,53 @@ +import MPCLogin from '@/components/common/ConnectWallet/MPCLogin' +import { AppRoutes } from '@/config/routes' +import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' +import SafeLogo from '@/public/images/logo-text.svg' +import css from './styles.module.css' +import { useRouter } from 'next/router' +import WalletLogin from './WalletLogin' +import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' +import Track from '@/components/common/Track' +import { trackEvent } from '@/services/analytics' + +const WelcomeLogin = () => { + const router = useRouter() + + const continueToCreation = () => { + trackEvent(CREATE_SAFE_EVENTS.CREATE_BUTTON) + router.push(AppRoutes.newSafe.create) + } + + return ( + + + + + Create Account + + + Choose how you would like to create your Safe Account + + + + + + or + + + + + + + Already have a Safe Account? + + + + Add existing one + + + + + ) +} + +export default WelcomeLogin diff --git a/src/components/welcome/WelcomeLogin/styles.module.css b/src/components/welcome/WelcomeLogin/styles.module.css new file mode 100644 index 0000000000..e9c586e392 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/styles.module.css @@ -0,0 +1,18 @@ +.loginCard { + width: 100%; + height: 100%; + display: flex; + border-radius: 6px; + justify-content: center; + align-items: center; +} + +.loginContent { + display: flex; + height: 100%; + width: 320px; + gap: var(--space-1); + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index 6b2c573afb..dedc5f6fcf 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -1,5 +1,4 @@ -.accordion :global .MuiAccordionDetails-root > div > div:first-child, -.accordion :global .MuiAccordionSummary-expandIconWrapper { +.accordion :global .MuiAccordionDetails-root > div > div:first-child { display: none; } @@ -22,6 +21,10 @@ .sidebar { max-height: calc(100vh - var(--header-height) - var(--footer-height) - 8px); overflow-y: auto; + align-self: flex-start; + margin-top: -24px; + margin-bottom: auto; + width: 100%; } .content { @@ -33,10 +36,15 @@ display: flex; flex-direction: column; justify-content: center; + gap: var(--space-2); width: 100%; height: 100%; } +.checkIcon { + color: var(--color-static-main); +} + .createAddCard { display: flex; flex-direction: column; diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts index c6af1f84dd..2b7cf6ec42 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts @@ -79,7 +79,7 @@ describe('useMPCWallet', () => { it('should have state NOT_INITIALIZED initially', () => { const { result } = renderHook(() => useMPCWallet()) expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) - expect(result.current.userInfo.email).toBeUndefined() + expect(result.current.userInfo?.email).toBeUndefined() }) describe('triggerLogin', () => { diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index 9fc69b4d06..9b32f78056 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -2,7 +2,7 @@ import { useState } from 'react' import useMPC from './useMPC' import BN from 'bn.js' import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' -import { COREKIT_STATUS, getWebBrowserFactor } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS, getWebBrowserFactor, type UserInfo } from '@web3auth/mpc-core-kit' import useOnboard, { connectWallet } from '../useOnboard' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { SecurityQuestionRecovery } from './recovery/SecurityQuestionRecovery' @@ -21,9 +21,7 @@ export type MPCWalletHook = { walletState: MPCWalletState triggerLogin: () => Promise resetAccount: () => Promise - userInfo: { - email: string | undefined - } + userInfo: UserInfo | undefined } export const useMPCWallet = (): MPCWalletHook => { @@ -81,19 +79,19 @@ export const useMPCWallet = (): MPCWalletHook => { } } - finalizeLogin() + await finalizeLogin() } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) } } - const finalizeLogin = () => { + const finalizeLogin = async () => { if (!mpcCoreKit || !onboard) { return } if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - connectWallet(onboard, { + await connectWallet(onboard, { autoSelect: { label: ONBOARD_MPC_MODULE_LABEL, disableModals: true, @@ -120,7 +118,7 @@ export const useMPCWallet = (): MPCWalletHook => { await deviceShareRecovery.createAndStoreDeviceFactor() } - finalizeLogin() + await finalizeLogin() } } @@ -130,8 +128,6 @@ export const useMPCWallet = (): MPCWalletHook => { recoverFactorWithPassword, resetAccount: criticalResetAccount, upsertPasswordBackup: () => Promise.resolve(), - userInfo: { - email: mpcCoreKit?.state.userInfo?.email, - }, + userInfo: mpcCoreKit?.state.userInfo, } } From 1a161daae4bb93d210956449212e5f43656dd00d Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 6 Oct 2023 19:22:14 +0200 Subject: [PATCH 07/52] feat: add sidebar design to welcome page (#2601) --- src/components/common/PageLayout/index.tsx | 1 - src/components/welcome/NewSafe.tsx | 123 +++++++++--------- .../welcome/WelcomeLogin/styles.module.css | 1 + src/components/welcome/styles.module.css | 23 ++-- 4 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/components/common/PageLayout/index.tsx b/src/components/common/PageLayout/index.tsx index 223800c653..317150384b 100644 --- a/src/components/common/PageLayout/index.tsx +++ b/src/components/common/PageLayout/index.tsx @@ -17,7 +17,6 @@ const isNoSidebarRoute = (pathname: string): boolean => { AppRoutes.share.safeApp, AppRoutes.newSafe.create, AppRoutes.newSafe.load, - AppRoutes.welcome, AppRoutes.index, AppRoutes.imprint, AppRoutes.privacy, diff --git a/src/components/welcome/NewSafe.tsx b/src/components/welcome/NewSafe.tsx index 02994badc6..2ef93fc43c 100644 --- a/src/components/welcome/NewSafe.tsx +++ b/src/components/welcome/NewSafe.tsx @@ -1,84 +1,87 @@ import React, { useState } from 'react' -import { Accordion, AccordionDetails, AccordionSummary, Box, Grid, SvgIcon, Typography } from '@mui/material' +import { Accordion, AccordionSummary, Box, Drawer, Grid, IconButton, SvgIcon, Typography } from '@mui/material' import SafeList from '@/components/sidebar/SafeList' import css from './styles.module.css' import CheckFilled from '@/public/images/common/check-filled.svg' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' import { DataWidget } from '@/components/welcome/DataWidget' import WelcomeLogin from './WelcomeLogin' import { useAppSelector } from '@/store' import { selectTotalAdded } from '@/store/addedSafesSlice' -const NewSafe = () => { - const [expanded, setExpanded] = useState(false) +import drawerCSS from '@/components/sidebar/Sidebar/styles.module.css' + +const BulletListItem = ({ text }: { text: string }) => ( +
  • + + + {text} + +
  • +) +const NewSafe = () => { const addedSafes = useAppSelector(selectTotalAdded) - const toggleSafeList = () => { - return setExpanded((prev) => !prev) - } + const [showSidebar, setShowSidebar] = useState(false) + + const closeSidebar = () => setShowSidebar(false) return ( - - - - - -
    - {addedSafes > 0 && ( - - - - - }> - - My Safe Accounts ({addedSafes}) - - + <> + +
    + - event.stopPropagation()}> - - - - - - +
    + +
    +
    +
    + + + + + +
    + {addedSafes > 0 && ( + + + + setShowSidebar(true)} expanded={false}> + + + + + + + My Safe Accounts ({addedSafes}) + + + + + - - )} - - Unlock a new way of ownership - + )} + + Unlock a new way of ownership + - - The most trusted decentralized custody protocol and collective asset management platform. - + + The most trusted decentralized custody protocol and collective asset management platform. + - - - - - Stealth security with multiple owners - - - - - - Make it yours with modules and guards - - - - - - Access 130+ ecosystem apps - - - -
    +
      + + + +
    +
    +
    -
    + ) } diff --git a/src/components/welcome/WelcomeLogin/styles.module.css b/src/components/welcome/WelcomeLogin/styles.module.css index e9c586e392..ba47057527 100644 --- a/src/components/welcome/WelcomeLogin/styles.module.css +++ b/src/components/welcome/WelcomeLogin/styles.module.css @@ -5,6 +5,7 @@ border-radius: 6px; justify-content: center; align-items: center; + padding: var(--space-2); } .loginContent { diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index dedc5f6fcf..cb91034710 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -9,9 +9,8 @@ .accordion :global .MuiAccordionSummary-root { background: transparent !important; - padding: var(--space-3); + padding: var(--space-2); border-bottom: 1px solid var(--color-border-light); - pointer-events: none; } .accordion :global .MuiAccordionSummary-content { @@ -22,17 +21,29 @@ max-height: calc(100vh - var(--header-height) - var(--footer-height) - 8px); overflow-y: auto; align-self: flex-start; - margin-top: -24px; margin-bottom: auto; width: 100%; } +.bulletList { + list-style: none; + margin-bottom: auto; +} + +.bulletList li { + display: flex; + flex-direction: row; + gap: var(--space-1); + align-items: center; + margin-bottom: var(--space-2); +} + .content { background: linear-gradient(-90deg, #10ff84, #5fddff); background-size: 200% 200%; animation: gradient 15s ease infinite; border-radius: 6px; - padding: var(--space-8); + padding: var(--space-3); display: flex; flex-direction: column; justify-content: center; @@ -84,10 +95,6 @@ height: auto; } - .accordion :global .MuiAccordionSummary-root { - pointer-events: auto; - } - .accordion :global .MuiAccordionSummary-expandIconWrapper { display: block; } From e682af60c6f8209416dda5630fbc2e6c773bc02a Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 9 Oct 2023 09:29:26 +0200 Subject: [PATCH 08/52] fix: layout without added Safes --- src/components/welcome/NewSafe.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/welcome/NewSafe.tsx b/src/components/welcome/NewSafe.tsx index 2ef93fc43c..f293cc25d4 100644 --- a/src/components/welcome/NewSafe.tsx +++ b/src/components/welcome/NewSafe.tsx @@ -45,8 +45,8 @@ const NewSafe = () => {
    - {addedSafes > 0 && ( - + + {addedSafes > 0 && ( setShowSidebar(true)} expanded={false}> @@ -63,8 +63,9 @@ const NewSafe = () => { - - )} + )} + + Unlock a new way of ownership From 5ab369e3adb5a75b35df28afc6115196b518e31f Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 9 Oct 2023 17:29:51 +0200 Subject: [PATCH 09/52] feat: enable manualSync, commit MFA changes in batches (#2600) Co-authored-by: Usame Algan --- package.json | 2 +- .../SignerAccountMFA/PasswordForm.tsx | 32 ++++------------- .../settings/SignerAccountMFA/helper.ts | 25 ++----------- .../mpc/__tests__/useMPCWallet.test.ts | 7 ++-- .../mpc/recovery/DeviceShareRecovery.ts | 12 +++++-- src/hooks/wallets/mpc/useMPC.ts | 1 + src/hooks/wallets/mpc/useMPCWallet.ts | 3 ++ yarn.lock | 36 +++++++++---------- 8 files changed, 47 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index 1000a072d2..cadb9b852e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.7", - "@web3auth/mpc-core-kit": "^1.0.2", + "@web3auth/mpc-core-kit": "^1.1.0", "blo": "^1.1.1", "bn.js": "^5.2.1", "classnames": "^2.3.1", diff --git a/src/components/settings/SignerAccountMFA/PasswordForm.tsx b/src/components/settings/SignerAccountMFA/PasswordForm.tsx index 0f4d905974..f40e2e0522 100644 --- a/src/components/settings/SignerAccountMFA/PasswordForm.tsx +++ b/src/components/settings/SignerAccountMFA/PasswordForm.tsx @@ -1,40 +1,33 @@ -import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery' import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' -import { Typography, TextField, FormControlLabel, Checkbox, Button, Box } from '@mui/material' +import { Typography, TextField, Button, Box } from '@mui/material' import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { useState, useMemo } from 'react' -import { Controller, useForm } from 'react-hook-form' +import { useForm } from 'react-hook-form' import { enableMFA } from './helper' enum PasswordFieldNames { oldPassword = 'oldPassword', newPassword = 'newPassword', confirmPassword = 'confirmPassword', - storeDeviceShare = 'storeDeviceShare', } type PasswordFormData = { [PasswordFieldNames.oldPassword]: string | undefined [PasswordFieldNames.newPassword]: string [PasswordFieldNames.confirmPassword]: string - [PasswordFieldNames.storeDeviceShare]: boolean } export const PasswordForm = ({ mpcCoreKit }: { mpcCoreKit: Web3AuthMPCCoreKit }) => { const formMethods = useForm({ mode: 'all', - defaultValues: async () => { - const isDeviceShareStored = await new DeviceShareRecovery(mpcCoreKit).isEnabled() - return { - [PasswordFieldNames.confirmPassword]: '', - [PasswordFieldNames.oldPassword]: undefined, - [PasswordFieldNames.newPassword]: '', - [PasswordFieldNames.storeDeviceShare]: isDeviceShareStored, - } + defaultValues: { + [PasswordFieldNames.confirmPassword]: '', + [PasswordFieldNames.oldPassword]: undefined, + [PasswordFieldNames.newPassword]: '', }, }) - const { register, formState, getValues, control, handleSubmit } = formMethods + const { register, formState, getValues, handleSubmit } = formMethods const [enablingMFA, setEnablingMFA] = useState(false) @@ -100,17 +93,6 @@ export const PasswordForm = ({ mpcCoreKit }: { mpcCoreKit: Web3AuthMPCCoreKit }) })} /> - ( - } - label="Do not ask for second factor on this device" - /> - )} - /> -
    - + ) : ( - + + + or + + diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx index 41952da2b2..a98afa4d4e 100644 --- a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -54,7 +54,7 @@ describe('MPCLogin', () => { const mockOnLogin = jest.fn() const walletAddress = hexZeroPad('0x1', 20) const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) - const mockTriggerLogin = jest.fn() + const mockTriggerLogin = jest.fn(() => true) const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ userInfo: { email: undefined, diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index aa127889c6..b6aa736f13 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -12,11 +12,11 @@ .popoverContainer { padding: var(--space-2); - width: 250px; + width: 300px; display: flex; flex-direction: column; align-items: center; - gap: var(--space-2); + gap: var(--space-1); border: 1px solid var(--color-border-light); } @@ -66,9 +66,30 @@ gap: var(--space-2); } +.loginButton { + min-height: 42px; +} + +.accountContainer { + width: 100%; + margin-bottom: var(--space-1); +} + +.accountContainer > span { + border-radius: 8px 8px 0 0; +} + +.addressContainer { + border-radius: 0 0 8px 8px; + padding: 12px; + border: 1px solid var(--color-border-light); + border-top: 0; + font-size: 14px; +} + @media (max-width: 599.95px) { - .buttonContainer { - transform: scale(0.8); + .socialLoginInfo > div > div { + display: none; } .notConnected { diff --git a/src/components/common/ConnectWallet/useConnectWallet.ts b/src/components/common/ConnectWallet/useConnectWallet.ts index 9dc10e7930..b3f832229e 100644 --- a/src/components/common/ConnectWallet/useConnectWallet.ts +++ b/src/components/common/ConnectWallet/useConnectWallet.ts @@ -1,19 +1,17 @@ -import { useMemo } from 'react' +import { useCallback } from 'react' import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' -const useConnectWallet = (): (() => void) => { +const useConnectWallet = () => { const onboard = useOnboard() - return useMemo(() => { + return useCallback(() => { if (!onboard) { - return () => {} + return Promise.resolve(undefined) } - return () => { - trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) - connectWallet(onboard) - } + trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) + return connectWallet(onboard) }, [onboard]) } diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx new file mode 100644 index 0000000000..8615a8a3be --- /dev/null +++ b/src/components/common/SocialLoginInfo/index.tsx @@ -0,0 +1,52 @@ +import { Box, Typography } from '@mui/material' +import css from './styles.module.css' +import { useContext } from 'react' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type ConnectedWallet } from '@/services/onboard' +import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' +import CopyAddressButton from '@/components/common/CopyAddressButton' +import ExplorerButton from '@/components/common/ExplorerButton' +import { getBlockExplorerLink } from '@/utils/chains' +import { useAppSelector } from '@/store' +import { selectSettings } from '@/store/settingsSlice' + +const SocialLoginInfo = ({ + wallet, + chainInfo, + hideActions = false, +}: { + wallet: ConnectedWallet + chainInfo?: ChainInfo + hideActions?: boolean +}) => { + const { userInfo } = useContext(MpcWalletContext) + const prefix = chainInfo?.shortName + const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined + const settings = useAppSelector(selectSettings) + + if (!userInfo) return <> + + return ( + + Profile Image +
    + + {userInfo.name} + + + {userInfo.email} + +
    + {!hideActions && ( +
    + + + + +
    + )} +
    + ) +} + +export default SocialLoginInfo diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css new file mode 100644 index 0000000000..6b694addb6 --- /dev/null +++ b/src/components/common/SocialLoginInfo/styles.module.css @@ -0,0 +1,23 @@ +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.text { + font-size: 12px; + line-height: 14px; +} + +.actionButtons { + display: flex; + justify-self: flex-end; + margin-left: auto; +} diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 758a3b23c7..451bf5d502 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -3,26 +3,16 @@ import useWallet from '@/hooks/wallets/useWallet' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { Box, Button, Typography } from '@mui/material' import { EthHashInfo } from '@safe-global/safe-react-components' -import { useState, useEffect } from 'react' -const WalletLogin = ({ onLogin }: { onLogin?: () => void }) => { +const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const wallet = useWallet() const connectWallet = useConnectWallet() - const [loginTriggered, setLoginTriggered] = useState(false) - const login = async () => { - setLoginTriggered(true) await connectWallet() + onLogin() } - // If login was triggered through the Button we immediately continue if logged in - useEffect(() => { - if (loginTriggered && wallet && onLogin) { - onLogin() - } - }, [loginTriggered, onLogin, wallet]) - if (wallet !== null && wallet?.label !== ONBOARD_MPC_MODULE_LABEL) { return ( ) diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index cb91034710..2b8c377b76 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -28,6 +28,7 @@ .bulletList { list-style: none; margin-bottom: auto; + padding: 0; } .bulletList li { diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index c8c9a2d9b0..caa61fe945 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -17,9 +17,9 @@ export enum MPCWalletState { export type MPCWalletHook = { upsertPasswordBackup: (password: string) => Promise - recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise + recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise walletState: MPCWalletState - triggerLogin: () => Promise + triggerLogin: () => Promise resetAccount: () => Promise userInfo: UserInfo | undefined } @@ -74,15 +74,17 @@ export const useMPCWallet = (): MPCWalletHook => { const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) if (securityQuestions.isEnabled()) { setWalletState(MPCWalletState.MANUAL_RECOVERY) - return + return false } } } await finalizeLogin() + return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) + return false } } @@ -123,6 +125,8 @@ export const useMPCWallet = (): MPCWalletHook => { await finalizeLogin() } + + return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN } return { From daf6bdb1aa8ae23e511bee80dc313bbd340bd00b Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Thu, 12 Oct 2023 20:58:39 +0200 Subject: [PATCH 12/52] fix: Enable 1-click safe create for social login (#2620) --- .../common/ConnectWallet/AccountCenter.tsx | 8 +- .../common/ConnectWallet/MPCLogin.tsx | 28 +++- .../ConnectWallet/__tests__/MPCLogin.test.tsx | 61 ++++---- .../common/NetworkSelector/index.tsx | 73 ++++----- .../common/NetworkSelector/styles.module.css | 4 + src/components/common/WalletInfo/index.tsx | 12 ++ .../common/WalletInfo/styles.module.css | 4 + src/components/new-safe/create/index.tsx | 5 + .../create/steps/ReviewStep/index.test.tsx | 41 +++++ .../create/steps/ReviewStep/index.tsx | 142 ++++++++++++------ .../create/steps/SetNameStep/index.tsx | 11 +- 11 files changed, 268 insertions(+), 121 deletions(-) create mode 100644 src/components/new-safe/create/steps/ReviewStep/index.test.tsx diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 6f3ac1d140..a4208e8af1 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -55,13 +55,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { <> - {wallet.label === ONBOARD_MPC_MODULE_LABEL ? ( -
    - -
    - ) : ( - - )} + {open ? : } diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx index 03ead96b6e..34ca5b51c4 100644 --- a/src/components/common/ConnectWallet/MPCLogin.tsx +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -4,16 +4,24 @@ import { useContext } from 'react' import { MpcWalletContext } from './MPCWalletProvider' import { PasswordRecovery } from './PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' +import InfoIcon from '@/public/images/notifications/info.svg' import css from './styles.module.css' import useWallet from '@/hooks/wallets/useWallet' +import { useCurrentChain } from '@/hooks/useChains' +import chains from '@/config/chains' const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { + const currentChain = useCurrentChain() const { triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) const wallet = useWallet() const loginPending = walletState === MPCWalletState.AUTHENTICATING + // TODO: Replace with feature flag from config service + const isMPCLoginEnabled = currentChain?.chainId === chains.gno + const isDisabled = loginPending || !isMPCLoginEnabled + const login = async () => { const success = await triggerLogin() @@ -39,7 +47,7 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { sx={{ px: 2, py: 1, borderWidth: '1px !important' }} onClick={onLogin} size="small" - disabled={loginPending} + disabled={isDisabled} fullWidth > @@ -64,7 +72,7 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { variant="outlined" onClick={login} size="small" - disabled={loginPending} + disabled={isDisabled} fullWidth sx={{ borderWidth: '1px !important' }} > @@ -74,6 +82,22 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { )} + {!isMPCLoginEnabled && ( + + + Currently only supported on Gnosis Chain + + )} + {walletState === MPCWalletState.MANUAL_RECOVERY && ( )} diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx index a98afa4d4e..47f56a9ada 100644 --- a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -1,20 +1,24 @@ -import { act, render, waitFor } from '@/tests/test-utils' +import { render, waitFor } from '@/tests/test-utils' import * as useWallet from '@/hooks/wallets/useWallet' import * as useMPCWallet from '@/hooks/wallets/mpc/useMPCWallet' +import * as chains from '@/hooks/useChains' + import MPCLogin from '../MPCLogin' import { hexZeroPad } from '@ethersproject/bytes' import { type EIP1193Provider } from '@web3-onboard/common' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { MpcWalletProvider } from '../MPCWalletProvider' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' describe('MPCLogin', () => { beforeEach(() => { jest.resetAllMocks() }) - it('should render continue with connected account', async () => { + it('should render continue with connected account when on gnosis chain', async () => { const mockOnLogin = jest.fn() const walletAddress = hexZeroPad('0x1', 20) + jest.spyOn(chains, 'useCurrentChain').mockReturnValue({ chainId: '100' } as unknown as ChainInfo) jest.spyOn(useWallet, 'default').mockReturnValue({ address: walletAddress, chainId: '5', @@ -50,19 +54,13 @@ describe('MPCLogin', () => { expect(mockOnLogin).toHaveBeenCalled() }) - it('should render google login button and invoke the callback on connection if no wallet is connected', async () => { + it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => { const mockOnLogin = jest.fn() - const walletAddress = hexZeroPad('0x1', 20) - const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) + jest.spyOn(chains, 'useCurrentChain').mockReturnValue({ chainId: '100' } as unknown as ChainInfo) + jest.spyOn(useWallet, 'default').mockReturnValue(null) const mockTriggerLogin = jest.fn(() => true) - const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ - userInfo: { - email: undefined, - name: undefined, - profileImage: undefined, - }, + jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ triggerLogin: mockTriggerLogin, - walletState: useMPCWallet.MPCWalletState.NOT_INITIALIZED, } as unknown as useMPCWallet.MPCWalletHook) const result = render( @@ -78,30 +76,27 @@ describe('MPCLogin', () => { // We do not automatically invoke the callback as the user did not actively connect expect(mockOnLogin).not.toHaveBeenCalled() - await act(async () => { - // Click the button and mock a successful login - const button = await result.findByRole('button') - button.click() - mockUseMPCWallet.mockReset().mockReturnValue({ - userInfo: { - email: 'test@safe.test', - name: 'Test Testermann', - profileImage: 'test.png', - }, - triggerLogin: jest.fn(), - walletState: useMPCWallet.MPCWalletState.READY, - } as unknown as useMPCWallet.MPCWalletHook) - - mockUseWallet.mockReset().mockReturnValue({ - address: walletAddress, - chainId: '5', - label: ONBOARD_MPC_MODULE_LABEL, - provider: {} as unknown as EIP1193Provider, - }) - }) + const button = await result.findByRole('button') + button.click() await waitFor(() => { expect(mockOnLogin).toHaveBeenCalled() }) }) + + it('should disable the Google Login button with a message when not on gnosis chain', async () => { + jest.spyOn(chains, 'useCurrentChain').mockReturnValue({ chainId: '1' } as unknown as ChainInfo) + jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ + triggerLogin: jest.fn(), + } as unknown as useMPCWallet.MPCWalletHook) + + const result = render( + + + , + ) + + expect(result.getByText('Currently only supported on Gnosis Chain')).toBeInTheDocument() + expect(await result.findByRole('button')).toBeDisabled() + }) }) diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 651f7c50f9..d32365b8d0 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import type { SelectChangeEvent } from '@mui/material' -import { MenuItem, Select, Skeleton } from '@mui/material' +import { FormControl, MenuItem, Select, Skeleton } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import useChains from '@/hooks/useChains' import { useRouter } from 'next/router' @@ -11,10 +11,13 @@ import type { ReactElement } from 'react' import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' +import useWallet from '@/hooks/wallets/useWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' const keepPathRoutes = [AppRoutes.welcome, AppRoutes.newSafe.create, AppRoutes.newSafe.load] const NetworkSelector = (): ReactElement => { + const wallet = useWallet() const { configs } = useChains() const chainId = useChainId() const router = useRouter() @@ -54,40 +57,42 @@ const NetworkSelector = (): ReactElement => { } return configs.length ? ( - palette.border.main, - }, - }} - > - {configs.map((chain) => { - return ( - - - - - - ) - })} - + }} + sx={{ + '& .MuiSelect-select': { + py: 0, + }, + '& svg path': { + fill: ({ palette }) => palette.border.main, + }, + }} + > + {configs.map((chain) => { + return ( + + + + + + ) + })} + + ) : ( ) diff --git a/src/components/common/NetworkSelector/styles.module.css b/src/components/common/NetworkSelector/styles.module.css index a24929387c..4078402a66 100644 --- a/src/components/common/NetworkSelector/styles.module.css +++ b/src/components/common/NetworkSelector/styles.module.css @@ -24,6 +24,10 @@ margin-right: var(--space-2); } +.select :global .Mui-disabled { + pointer-events: none; +} + .newChip { font-weight: bold; letter-spacing: -0.1px; diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index 8cbd0c8f2a..5f71565b85 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -9,6 +9,8 @@ import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' import css from './styles.module.css' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import SocialLoginInfo from '@/components/common/SocialLoginInfo' export const UNKNOWN_CHAIN_NAME = 'Unknown' @@ -16,6 +18,16 @@ const WalletInfo = ({ wallet }: { wallet: ConnectedWallet }): ReactElement => { const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = walletChain?.shortName + const isSocialLogin = wallet.label === ONBOARD_MPC_MODULE_LABEL + + if (isSocialLogin) { + return ( +
    + +
    + ) + } + return ( diff --git a/src/components/common/WalletInfo/styles.module.css b/src/components/common/WalletInfo/styles.module.css index 8b7242711c..96ddc9f7b5 100644 --- a/src/components/common/WalletInfo/styles.module.css +++ b/src/components/common/WalletInfo/styles.module.css @@ -31,4 +31,8 @@ .walletName { display: none; } + + .socialLoginInfo > div > div { + display: none; + } } diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index f2b179d593..0bbe041f4b 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -19,6 +19,7 @@ import CreateSafeInfos from '@/components/new-safe/create/CreateSafeInfos' import { type ReactElement, useMemo, useState } from 'react' import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' export type NewSafeFormData = { name: string @@ -163,6 +164,9 @@ const CreateSafe = () => { saltNonce: Date.now(), } + // Jump to review screen when using social login + const initialStep = wallet?.label === ONBOARD_MPC_MODULE_LABEL ? 2 : 0 + const onClose = () => { router.push(AppRoutes.welcome) } @@ -178,6 +182,7 @@ const CreateSafe = () => { { + it('should display the total fee if not social login', () => { + jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet) + const mockTotalFee = '0.0123' + const result = render() + + expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument() + }) + + it('displays a sponsored by message for social login', () => { + jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet) + const result = render() + + expect(result.getByText(/Your account is sponsored by Gnosis Chain/)).toBeInTheDocument() + }) + + it('displays an error message for social login if there are no relays left', () => { + jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet) + const result = render() + + expect( + result.getByText(/You have used up your 5 free transactions per hour. Please try again later/), + ).toBeInTheDocument() + }) +}) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 0699dc6ae1..a2965116d9 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { Button, Grid, Typography, Divider, Box } from '@mui/material' +import { Button, Grid, Typography, Divider, Box, Alert } from '@mui/material' import { lightPalette } from '@safe-global/safe-react-components' import ChainIndicator from '@/components/common/ChainIndicator' import EthHashInfo from '@/components/common/EthHashInfo' @@ -21,12 +21,75 @@ import NetworkWarning from '@/components/new-safe/create/NetworkWarning' import useIsWrongChain from '@/hooks/useIsWrongChain' import ReviewRow from '@/components/new-safe/ReviewRow' import { ExecutionMethodSelector, ExecutionMethod } from '@/components/tx/ExecutionMethodSelector' -import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' +import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays' import classnames from 'classnames' import { hasRemainingRelays } from '@/utils/relaying' import { BigNumber } from 'ethers' import { usePendingSafe } from '../StatusStep/usePendingSafe' import { LATEST_SAFE_VERSION } from '@/config/constants' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { SPONSOR_LOGOS } from '@/components/tx/SponsoredBy' +import Image from 'next/image' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' + +export const NetworkFee = ({ + totalFee, + chain, + willRelay, +}: { + totalFee: string + chain: ChainInfo | undefined + willRelay: boolean +}) => { + const wallet = useWallet() + + const isSocialLogin = wallet?.label === ONBOARD_MPC_MODULE_LABEL + + if (!isSocialLogin) { + return ( + + + + ≈ {totalFee} {chain?.nativeCurrency.symbol} + + + + ) + } + + if (willRelay) { + return ( + <> + Free + + Your account is sponsored by + {chain?.chainName{' '} + Gnosis Chain + + + ) + } + + return ( + + You have used up your {MAX_HOUR_RELAYS} free transactions per hour. Please try again later. + + ) +} const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { const isWrongChain = useIsWrongChain() @@ -105,12 +168,15 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps } /> - {data.name}} /> + {data.name && {data.name}} />} - - - {canRelay && ( - - - } - /> - - )} - + + {canRelay && !isSocialLogin && ( + - - - - ≈ {totalFee} {chain?.nativeCurrency.symbol} - - - - {willRelay ? null : ( - - You will have to confirm a transaction with your connected wallet. - - )} - + } /> + )} + + + + + + {!willRelay && !isSocialLogin && ( + + You will have to confirm a transaction with your connected wallet. + + )} + + } + /> {isWrongChain && } + + - diff --git a/src/components/new-safe/create/steps/SetNameStep/index.tsx b/src/components/new-safe/create/steps/SetNameStep/index.tsx index 01cb0d9d31..fae5a2a7c0 100644 --- a/src/components/new-safe/create/steps/SetNameStep/index.tsx +++ b/src/components/new-safe/create/steps/SetNameStep/index.tsx @@ -16,6 +16,7 @@ import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { AppRoutes } from '@/config/routes' import MUILink from '@mui/material/Link' import Link from 'next/link' +import { useRouter } from 'next/router' type SetNameStepForm = { name: string @@ -33,6 +34,7 @@ function SetNameStep({ setStep, setSafeName, }: StepRenderProps & { setSafeName: (name: string) => void }) { + const router = useRouter() const fallbackName = useMnemonicSafeName() const isWrongChain = useIsWrongChain() useSyncSafeCreationStep(setStep) @@ -59,6 +61,10 @@ function SetNameStep({ } } + const onCancel = () => { + router.push(AppRoutes.welcome) + } + const isDisabled = isWrongChain || !isValid return ( @@ -109,7 +115,10 @@ function SetNameStep({ - + + From 3bec1818fd4ac40cf049da12ec349774e924dc66 Mon Sep 17 00:00:00 2001 From: schmanu Date: Tue, 17 Oct 2023 13:27:23 +0200 Subject: [PATCH 13/52] tests: apply transformIgnorePatterns after nextJest --- jest.config.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/jest.config.cjs b/jest.config.cjs index 5e492b7f63..3239b5f043 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -15,7 +15,6 @@ const customJestConfig = { '^.+\\.(svg)$': '/mocks/svg.js', isows: '/node_modules/isows/_cjs/index.js', }, - transformIgnorePatterns: ['node_modules/(?!isows/)'], testEnvironment: 'jest-environment-jsdom', testEnvironmentOptions: { url: 'http://localhost/balances?safe=rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A' }, globals: { From dcc5ad4c74e4aa668c0a8b81235e7730c331d48c Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 17 Oct 2023 13:38:10 +0200 Subject: [PATCH 14/52] fix: Update yarn lock --- yarn.lock | 820 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 805 insertions(+), 15 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5d610da99d..f19a92c25a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1035,7 +1035,7 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.10", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== @@ -1081,6 +1081,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@chaitanyapotti/register-service-worker@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@chaitanyapotti/register-service-worker/-/register-service-worker-1.7.3.tgz#0e670b8a4de1c319a147700d7ebb4f8a2b2e1b84" + integrity sha512-ywnUSfwvqdHchO3ELFWP6hlnhzc2UUETbk+TaBT/vicuMnJbnBLuDCcoy3aWvUE9bjmzg4QQpssRLSz1iZ7XRA== + "@coinbase/wallet-sdk@^3.7.2": version "3.7.2" resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.7.2.tgz#7a89bd9e3a06a1f26d4480d8642af33fb0c7e3aa" @@ -1485,11 +1490,24 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.5" -"@ethereumjs/rlp@^4.0.1": +"@ethereumjs/common@^3.0.0", "@ethereumjs/common@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.2.0.tgz#b71df25845caf5456449163012074a55f048e0a0" + integrity sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA== + dependencies: + "@ethereumjs/util" "^8.1.0" + crc-32 "^1.2.0" + +"@ethereumjs/rlp@^4.0.0", "@ethereumjs/rlp@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== +"@ethereumjs/rlp@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.0.tgz#dd81b32b2237bc32fb1b54534f8ff246a6c89d9b" + integrity sha512-WuS1l7GJmB0n0HsXLozCoEFc9IwYgf3l0gCkKVYgR67puVF1O4OpEaN0hWmm1c+iHUHFCKt1hJrvy5toLg+6ag== + "@ethereumjs/tx@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.0.0.tgz#8dfd91ed6e91e63996e37b3ddc340821ebd48c81" @@ -1522,7 +1540,17 @@ "@ethereumjs/common" "^2.6.4" ethereumjs-util "^7.1.5" -"@ethereumjs/util@^8.1.0": +"@ethereumjs/tx@^4.0.0", "@ethereumjs/tx@^4.1.2": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.2.0.tgz#5988ae15daf5a3b3c815493bc6b495e76009e853" + integrity sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw== + dependencies: + "@ethereumjs/common" "^3.2.0" + "@ethereumjs/rlp" "^4.0.1" + "@ethereumjs/util" "^8.1.0" + ethereum-cryptography "^2.0.0" + +"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.0.6", "@ethereumjs/util@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== @@ -1531,6 +1559,14 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" +"@ethereumjs/util@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-9.0.0.tgz#ac5945c629f3ab2ac584d8b12a8513e8eac29dc4" + integrity sha512-V8062I+ZXfFxtFLp7xsPeiT1IxDaVOZaM78nGj1gsWUFeZ8SgADMLDKWehp+muTy1JRbVoXFljZ1qoyv9ji/2g== + dependencies: + "@ethereumjs/rlp" "^5.0.0" + ethereum-cryptography "^2.1.2" + "@ethersproject/abi@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" @@ -3261,6 +3297,18 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@metamask/eth-sig-util@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz#a47f62800ee1917fef976ba67544a0ccd7d1bd6b" + integrity sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ== + dependencies: + "@ethereumjs/util" "^8.0.6" + bn.js "^4.12.0" + ethereum-cryptography "^2.0.0" + ethjs-util "^0.1.6" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + "@metamask/obs-store@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3" @@ -3269,6 +3317,22 @@ "@metamask/safe-event-emitter" "^2.0.0" through2 "^2.0.3" +"@metamask/rpc-errors@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-5.1.1.tgz#f82732ad0952d34d219eca42699c0c74bee95a9e" + integrity sha512-JjZnDi2y2CfvbohhBl+FOQRzmFlJpybcQlIk37zEX8B96eVSPbH/T8S0p7cSF8IE33IWx6JkD8Ycsd+2TXFxCw== + dependencies: + "@metamask/utils" "^5.0.0" + fast-safe-stringify "^2.0.6" + +"@metamask/rpc-errors@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.1.0.tgz#dfdef7cba4b9ad01ca3f99e990b5980575b89b4f" + integrity sha512-JQElKxai26FpDyRKO/yH732wI+BV90i1u6pOuDOpdADSbppB2g1pPh3AGST1zkZqEE9eIKIUw8UdBQ4rp3VTSg== + dependencies: + "@metamask/utils" "^8.1.0" + fast-safe-stringify "^2.0.6" + "@metamask/safe-event-emitter@2.0.0", "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c" @@ -3284,6 +3348,29 @@ semver "^7.3.8" superstruct "^1.0.3" +"@metamask/utils@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.2.tgz#140ba5061d90d9dac0280c19cab101bc18c8857c" + integrity sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g== + dependencies: + "@ethereumjs/tx" "^4.1.2" + "@types/debug" "^4.1.7" + debug "^4.3.4" + semver "^7.3.8" + superstruct "^1.0.3" + +"@metamask/utils@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.1.0.tgz#b8e73f5b4696b1b668cf5c1421daad140a3f98ac" + integrity sha512-sFNpzBKRicDgM2ZuU6vrPROlqNGm8/jDsjc5WrU1RzCkAMc4Xr3vUUf8p59uQ6B09etUWNb8d2GTCbISdmH/Ug== + dependencies: + "@ethereumjs/tx" "^4.1.2" + "@noble/hashes" "^1.3.1" + "@types/debug" "^4.1.7" + debug "^4.3.4" + semver "^7.5.4" + superstruct "^1.0.3" + "@motionone/animation@^10.15.1", "@motionone/animation@^10.16.3": version "10.16.3" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.16.3.tgz#f5b71e27fd8b88b61f983adb0ed6c8e3e89281f9" @@ -3962,6 +4049,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@solana/buffer-layout@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -4312,11 +4404,431 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.1.tgz#27337d72046d5236b32fd977edee3f74c71d332f" integrity sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg== +"@tkey-mpc/chrome-storage@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/chrome-storage/-/chrome-storage-9.0.2.tgz#77a0e46df75c844ee9c5f4b07963d22edb44b71b" + integrity sha512-3yeliKVCJP77OhG5J93uj2/0XwASO6H8WqSZgAgbVxXbC6oPazKTv84YVw0rkXmLE7gkHqagcjNiNOQaV29VRw== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + webextension-polyfill "^0.10.0" + +"@tkey-mpc/common-types@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-8.2.2.tgz#194324a9ad92027c0f040896fa94fec096e83c5d" + integrity sha512-aN/LUXFh+t+4vf1zzlIIh3xd+M4HKzBgaJq6eVopPLMxaG941ZFyOq9Tbymzliq9lrFhA3ByEyPFI6Lunqzvbg== + dependencies: + "@toruslabs/eccrypto" "^3.0.0" + "@toruslabs/rss-client" "^1.5.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + serialize-error "^8.1.0" + ts-custom-error "^3.3.1" + web3-utils "^1.8.1" + +"@tkey-mpc/common-types@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/common-types/-/common-types-9.0.2.tgz#ad8fc7c905a27caf1c7f6fa724b14768b46e3de2" + integrity sha512-TJrij2Fq/klAcLlpECqxNUnzeLcChNmrjQdwYJXZKMBsyUo3jYmAGTAqA7/6leaHSZfW18DMHcLrsSeiHQw63g== + dependencies: + "@toruslabs/customauth" "^16.0.6" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/rss-client" "^1.5.0" + "@toruslabs/torus.js" "^11.0.6" + bn.js "^5.2.1" + elliptic "^6.5.4" + serialize-error "^8.1.0" + ts-custom-error "^3.3.1" + +"@tkey-mpc/core@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/core/-/core-9.0.2.tgz#ca659d5288e5e818c36dd0618d2903fc6a9e1349" + integrity sha512-sc3w+rjc37OSQwff0bpVlmUDX58hBKY8AOSOsuWJoMfA7l8CFWyQp7iRBiVIxIgOojfeosu0RUcq2Z8HsTKK0g== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/rss-client" "^1.5.0" + "@toruslabs/torus.js" "^11.0.6" + bn.js "^5.2.1" + elliptic "^6.5.4" + json-stable-stringify "^1.0.2" + +"@tkey-mpc/security-questions@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/security-questions/-/security-questions-9.0.2.tgz#9ed6f944713f08d290bba920738488451db083fe" + integrity sha512-ekxMnmdbwuDRlEuXhZcG4U2W1Y1NFSo10Ctb8Mrw9FNz0XR7lTIAcLn3dlvCQHRmKLT1hlziW8oby4nsMpqsbA== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + bn.js "^5.2.1" + ethereum-cryptography "^2.1.2" + +"@tkey-mpc/service-provider-base@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-base/-/service-provider-base-9.0.2.tgz#03dc78cd3684bd261a1135ab0dfeb08c9b7b74c2" + integrity sha512-Gk3xF1NFV/BqmwMNKDB//DNoN9nff4kGeNxXiVnlxhQKZuX9phzd/sxJljKc6tutOK5YdOcvot7ZuwJg2/vZjA== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + bn.js "^5.2.1" + elliptic "^6.5.4" + +"@tkey-mpc/service-provider-torus@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@tkey-mpc/service-provider-torus/-/service-provider-torus-9.0.3.tgz#fdfddf000c55fb22ac92c135db5d46c8d9615e2a" + integrity sha512-9/OjTqjruR5AWMwJPRuR+ZxsZXzt9jQfzfNtde13VHSjtogcLwT12U1QTHJ7tsIZYckBy3ZVDbNpIcTOzz70tQ== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + "@tkey-mpc/service-provider-base" "^9.0.2" + "@toruslabs/customauth" "^16.0.6" + "@toruslabs/torus.js" "^11.0.6" + bn.js "^5.2.1" + elliptic "^6.5.4" + +"@tkey-mpc/share-serialization@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/share-serialization/-/share-serialization-9.0.2.tgz#a303d5eed1e6d9665a8aede5324193682bf4760e" + integrity sha512-dtQFbWvV9A4y6SGpx72WaRcwSREf/Yld9f4+u7FWe81WY575N655SheqM08gYYEGZlcQ+TiCMN3C2rq9I+NpMw== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + "@types/create-hash" "1.2.2" + bn.js "^5.2.1" + ethereum-cryptography "^2.1.2" + +"@tkey-mpc/storage-layer-torus@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@tkey-mpc/storage-layer-torus/-/storage-layer-torus-9.0.2.tgz#a59067379ead3e110359209a004ab69356f7930e" + integrity sha512-qxQ+O9GerdaM66DVh8Qi7oIRjBbcsUcuP+F9P5YdH8J+XEFDMSjhuuBaQpU3xaMFxpTozkSv4Kh/zHWyQE2YeA== + dependencies: + "@tkey-mpc/common-types" "^9.0.2" + "@toruslabs/http-helpers" "^5.0.0" + base64url "3.0.1" + bn.js "^5.2.1" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@toruslabs/base-controllers@^2.2.6": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-2.9.0.tgz#e23f4228b5a90bf94ba9b0b27451f3024bd1acc4" + integrity sha512-rKc+bR4QB/wdbH0CxLZC5e2PUZcIgkr9yY7TMd3oIffDklaYBnsuC5ES2/rgK1aRUDRWz+qWbTwLqsY6PlT37Q== + dependencies: + "@ethereumjs/util" "^8.0.6" + "@toruslabs/broadcast-channel" "^6.2.0" + "@toruslabs/http-helpers" "^3.3.0" + "@toruslabs/openlogin-jrpc" "^4.0.0" + async-mutex "^0.4.0" + bignumber.js "^9.1.1" + bowser "^2.11.0" + eth-rpc-errors "^4.0.3" + json-rpc-random-id "^1.0.1" + lodash "^4.17.21" + loglevel "^1.8.1" + +"@toruslabs/base-controllers@^4.2.0": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@toruslabs/base-controllers/-/base-controllers-4.5.2.tgz#ce0a475203990525ec352d3b3283eca5b1b19cbb" + integrity sha512-DEaeKREzY8OJ74k6nuOP1x2a8Z1HeCfV2uQE835QhLKuCDgqRomDwCwRcrCGT3rqxT/AVhWmBj+ykO/XS/4lZw== + dependencies: + "@ethereumjs/util" "^9.0.0" + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/broadcast-channel" "^9.0.0" + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/openlogin-jrpc" "^5.1.0" + async-mutex "^0.4.0" + bignumber.js "^9.1.2" + bowser "^2.11.0" + lodash "^4.17.21" + loglevel "^1.8.1" + +"@toruslabs/base-session-manager@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/base-session-manager/-/base-session-manager-3.0.0.tgz#4302d0747ef71a8278af79e577cf53253c907cd5" + integrity sha512-+EqwizmSFkVEczUtaw+swbAxRIIxC/EaFE040rwfgC5fixaQMNLw2cVYXWN67Ra47wC9A7Om6xwQTuGFR+dy4w== + dependencies: + "@toruslabs/http-helpers" "^5.0.0" + +"@toruslabs/broadcast-channel@^6.2.0": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-6.3.1.tgz#d4b0a08c3a0fa88d42d7f33387ce9be928c2d4b2" + integrity sha512-BEtJQ+9bMfFoGuCsp5NmxyY+C980Ho+3BZIKSiYwRtl5qymJ+jMX5lsoCppoQblcb34dP6FwEjeFw80Y9QC/rw== + dependencies: + "@babel/runtime" "^7.21.0" + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/metadata-helpers" "^3.2.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.6.1" + unload "^2.4.1" + +"@toruslabs/broadcast-channel@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-8.0.0.tgz#904ee44fceeb861d16f282ca211d1e2adf16e2f8" + integrity sha512-qCyWsHVL4Xtx1J6k1+acD7TJKCelJWyUy5Q5zyiWMPxMGFxTv1XdRyqpzV+VgwbcslIqgFN0GewOry2l1jlUQQ== + dependencies: + "@babel/runtime" "^7.22.10" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.7.2" + unload "^2.4.1" + +"@toruslabs/broadcast-channel@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/broadcast-channel/-/broadcast-channel-9.0.0.tgz#8f13b829269ee410fd46b7b463dea33b0c116147" + integrity sha512-GA0hh32vt0qu0qZ/QepNHDT5bxKzTsDWu2yp5J1a8CQWNwUGPkYkDe1ycRySQVBo/wa7UyZdXvgLYtYne+/cZw== + dependencies: + "@babel/runtime" "^7.22.10" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + bowser "^2.11.0" + loglevel "^1.8.1" + oblivious-set "1.1.1" + socket.io-client "^4.7.2" + unload "^2.4.1" + +"@toruslabs/constants@^13.0.1", "@toruslabs/constants@^13.0.3": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@toruslabs/constants/-/constants-13.0.3.tgz#260a3e5c430d201bd3d92e13ef2044bd89a4432e" + integrity sha512-DiksceNFwzV4XBwcPdKpV6tfrvWIxhmwseTJbNnlzdy6uXgzvtagfapu+98pjrkNrjoRLiM17QBgBi8sNw7oGQ== + +"@toruslabs/customauth@^16.0.6": + version "16.0.6" + resolved "https://registry.yarnpkg.com/@toruslabs/customauth/-/customauth-16.0.6.tgz#cf3f0e614735fb799a9748b992026f81a9ee2ab6" + integrity sha512-4+Cxbxz3fxm750MMaMqMoJS0x8RZdD0y1tHLa/2T+b993cjRoQRixddTa3rWNYdbWsg2rgcmGkpXxywq3aLzwA== + dependencies: + "@chaitanyapotti/register-service-worker" "^1.7.3" + "@toruslabs/broadcast-channel" "^8.0.0" + "@toruslabs/constants" "^13.0.1" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/fetch-node-details" "^13.0.1" + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + "@toruslabs/torus.js" "^11.0.5" + base64url "^3.0.1" + bowser "^2.11.0" + events "^3.3.0" + jwt-decode "^3.1.2" + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/eccrypto@4.0.0", "@toruslabs/eccrypto@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-4.0.0.tgz#0b27ed2d1e9483e77f42a7619a2c3c19cb802f44" + integrity sha512-Z3EINkbsgJx1t6jCDVIJjLSUEGUtNIeDjhMWmeDGOWcP/+v/yQ1hEvd1wfxEz4q5WqIHhevacmPiVxiJ4DljGQ== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/eccrypto@^2.1.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-2.2.1.tgz#19012cc4e774e8c3df7ceebb2c1a07ecfd784917" + integrity sha512-7sviL0wLYsfA5ogEAOIdb0tu/QAOFXfHc9B8ONYtF04x4Mg3Nr89LL35FhjaEm055q8Ru7cUQhEFSiqJqm9GCw== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/eccrypto@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/eccrypto/-/eccrypto-3.0.0.tgz#96df18e9d7320b230492671cf2942e703cd34ae3" + integrity sha512-+lFjV+0FZ3S4zH5T/Gnc795HoqpzLLDW28fSkibZRlx1Nx8uwbl3pyJSfya0C0bRHH1/+NTeBogUDijaRJ1NCw== + dependencies: + elliptic "^6.5.4" + +"@toruslabs/fetch-node-details@^13.0.1": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@toruslabs/fetch-node-details/-/fetch-node-details-13.0.3.tgz#3cb1d08a61e7ded4eda325b1f52c837553903cd4" + integrity sha512-u8Lhp2rOcK9+0qfz74JzLIisjfDKbMY5H/yh9E601peBydshx9HnfWTuRuj5bYGuHCQlXHC3t7UQbMAiuZ0EuA== + dependencies: + "@toruslabs/constants" "^13.0.3" + "@toruslabs/fnd-base" "^13.0.3" + "@toruslabs/http-helpers" "^5.0.0" + loglevel "^1.8.1" + +"@toruslabs/fnd-base@^13.0.1", "@toruslabs/fnd-base@^13.0.3": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@toruslabs/fnd-base/-/fnd-base-13.0.3.tgz#11a0cc95b085309322a6e20818b5dfb14cf6b004" + integrity sha512-XQyUacMwQ/yb1OgEnZHtnepCIhh9DfeTz+Ki7DkpqiCPnvduXqQYPSYoLwQumBjHOKwn9HxHdPYL9ps3hQpGPg== + dependencies: + "@toruslabs/constants" "^13.0.3" + +"@toruslabs/http-helpers@^3.2.0", "@toruslabs/http-helpers@^3.3.0", "@toruslabs/http-helpers@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-3.4.0.tgz#6d1da9e6aba094af62e73cf639a69844c82202f3" + integrity sha512-CoeJSL32mpp0gmYjxv48odu6pfjHk/rbJHDwCtYPcMHAl+qUQ/DTpVOOn9U0fGkD+fYZrQmZbRkXFgLhiT0ajQ== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/http-helpers@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/http-helpers/-/http-helpers-5.0.0.tgz#2a309d2a2c5c00d50a725d83ccec8a7475771d62" + integrity sha512-GmezWz9JeF6YyhjLSm+9XDF4YaeICEckY0Jbo43i86SjhfJYgRWqEi63VSiNsaqc/z810Q0FQvEk1TnBRX2tgA== + dependencies: + lodash.merge "^4.6.2" + loglevel "^1.8.1" + +"@toruslabs/metadata-helpers@5.0.0", "@toruslabs/metadata-helpers@^5.0.0", "@toruslabs/metadata-helpers@^5.x": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-5.0.0.tgz#12be5de4e8a5d1af2dd080bdf05f5ad8953aaae7" + integrity sha512-ZUFfOHJVJC53c8wJYHjdF3bIgN2ZvfqehbTZ/zJ7oVFfrrd6O66V3gQ1i1zxBjH3yhOvZKQwc0DaMmh3G0NUXQ== + dependencies: + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/http-helpers" "^5.0.0" + elliptic "^6.5.4" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + +"@toruslabs/metadata-helpers@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@toruslabs/metadata-helpers/-/metadata-helpers-3.2.0.tgz#b297933ac37481a9c86a125ac6a4e5c2f109fb78" + integrity sha512-2bCc6PNKd9y+aWfZQ1FXd47QmfyT4NmmqPGfsqk+sQS2o+MlxIyLuh9uh7deMgXo4b4qBDX+RQGbIKM1zVk56w== + dependencies: + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/http-helpers" "^3.4.0" + elliptic "^6.5.4" + ethereum-cryptography "^2.0.0" + json-stable-stringify "^1.0.2" + +"@toruslabs/openlogin-jrpc@^2.6.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-2.13.0.tgz#aae71e7c9b0161bc14baf3fc696605d74e0b99f4" + integrity sha512-TEg50/84xSocHLb3MEtw0DaIa+bXU66TJJjjDrqGPjoRo97fn8F8jDW2AcVV+eug39xpfxPIw1FFdCtgunmz7w== + dependencies: + "@toruslabs/openlogin-utils" "^2.13.0" + end-of-stream "^1.4.4" + eth-rpc-errors "^4.0.3" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^3.6.0" + +"@toruslabs/openlogin-jrpc@^4.0.0": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-4.7.2.tgz#e04dd6945da92d790f713a58aaa1657c57b330c8" + integrity sha512-9Eb0cPc0lPuS6v2YkQlgzfbRnZ6fLez9Ike5wznoHSFA2/JVu1onwuI56EV1HwswdDrOWPPQEyzI1j9NriZ0ew== + dependencies: + "@metamask/rpc-errors" "^5.1.1" + "@toruslabs/openlogin-utils" "^4.7.0" + end-of-stream "^1.4.4" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^4.4.2" + +"@toruslabs/openlogin-jrpc@^5.1.0", "@toruslabs/openlogin-jrpc@^5.2.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-jrpc/-/openlogin-jrpc-5.3.0.tgz#37fefe43d82b0ecfa7a99de56050e4b46bf68a5c" + integrity sha512-tM1XHG5UulvENhZYewo9eVWF5r9SffGru8+09qFED7g/PFupKs/rccaXYwyWRGVenQSMgj6ipgUd72zliuen+A== + dependencies: + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/openlogin-utils" "^5.3.0" + end-of-stream "^1.4.4" + events "^3.3.0" + fast-safe-stringify "^2.1.1" + once "^1.4.0" + pump "^3.0.0" + readable-stream "^4.4.2" + +"@toruslabs/openlogin-session-manager@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-session-manager/-/openlogin-session-manager-3.0.0.tgz#2f13af3ef96ddda48803a0265a798cffc3c9f78c" + integrity sha512-S+nnZQ+Y+XCHvTYaov3ltiV2hAAPpKpwxvB4TmbMvi7KWOZ8BcUJQykSITlIXV4aE5y5BD96rsmjQ3C3MyVtUQ== + dependencies: + "@toruslabs/base-session-manager" "^3.0.0" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "5.0.0" + +"@toruslabs/openlogin-utils@^2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-2.13.0.tgz#e339f9d638b1e3a8ecca7b8c973d6060a19afda5" + integrity sha512-g4pj6hIdKcuyetVsUWqiAJmCooTS9hOADL31m7LTqgdXzX9oR437A+c8Dw8gzFVcHmkK16Yt2//GvlKnSsGILg== + dependencies: + base64url "^3.0.1" + keccak "^3.0.3" + randombytes "^2.1.0" + +"@toruslabs/openlogin-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-4.7.0.tgz#741d6ba1c0754b59a182b1c6dd8d0263695ed980" + integrity sha512-w6XkHs4WKuufsf/zzteBzs4EJuOknrUmJ+iv5FZ8HzIpMQeL/984CP8HYaFSEYkbGCP4ydAnhY4Uh0QAhpDbPg== + dependencies: + base64url "^3.0.1" + +"@toruslabs/openlogin-utils@^5.2.0", "@toruslabs/openlogin-utils@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin-utils/-/openlogin-utils-5.3.0.tgz#59906be13fe47c399513360e36b9262410e5a1f2" + integrity sha512-WfwadC7ZqKOTVqfI4rFANu7IzEgI7H4A/0TiIQihxHpFWdFM23yVuCCIm5zCzQ1QtA2mwBbBZYwOJ0YHVrV6HQ== + dependencies: + "@toruslabs/constants" "^13.0.1" + base64url "^3.0.1" + +"@toruslabs/openlogin@^5.2.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@toruslabs/openlogin/-/openlogin-5.3.0.tgz#4cb28a62bf30b06b7e56a78bf9d7abe01d640560" + integrity sha512-I6bVBPDlqcnF/11+disyS0kUClJLavYcLi3dBEx2a1mEv18jOy1kuG+FSenDMzF733MCk1Y3Iikox0evPzhpvQ== + dependencies: + "@toruslabs/broadcast-channel" "^8.0.0" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/metadata-helpers" "^5.0.0" + "@toruslabs/openlogin-session-manager" "^3.0.0" + "@toruslabs/openlogin-utils" "^5.3.0" + bowser "^2.11.0" + events "^3.3.0" + loglevel "^1.8.1" + ts-custom-error "^3.3.1" + +"@toruslabs/rss-client@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@toruslabs/rss-client/-/rss-client-1.5.0.tgz#82a02736f671152e1a4c9078194320e2bbc4e49e" + integrity sha512-7oaRZjmomNoL0+OZMFe8mv3hSeaYWvrxTjuUtkOHybMitStEcEj64tPjWia646BX7AFeqtUCURNdNaQzqfwDMQ== + dependencies: + "@toruslabs/eccrypto" "^2.1.1" + "@toruslabs/http-helpers" "^3.2.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + fetch "^1.1.0" + loglevel "^1.8.1" + node-fetch "^2.0.0" + web3-eth-contract "^1.8.1" + web3-utils "^1.8.1" + +"@toruslabs/torus.js@^11.0.5", "@toruslabs/torus.js@^11.0.6": + version "11.0.6" + resolved "https://registry.yarnpkg.com/@toruslabs/torus.js/-/torus.js-11.0.6.tgz#982f28555922e66822b38d265c6f8a38bc094db3" + integrity sha512-l+Ba/DX1L2cLngiL08r8zZmRQ/A3MJ4pl20MaTzQuwxS2OfOXSReKld4YOWHwo5NEd36PHZdZ43qbh0NrzrwVQ== + dependencies: + "@toruslabs/constants" "^13.0.1" + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/http-helpers" "^5.0.0" + bn.js "^5.2.1" + elliptic "^6.5.4" + ethereum-cryptography "^2.1.2" + json-stable-stringify "^1.0.2" + loglevel "^1.8.1" + +"@toruslabs/tss-client@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@toruslabs/tss-client/-/tss-client-1.7.1.tgz#018511a75a1ee6094fbe74d2b7be968b92255e97" + integrity sha512-gaVjXy/eJKv59zdWW2lnAhjumw8DMMGlGHXEOipVywCylKqS8VvQVwwo+UyevR2VXUQsNkBr9deD5TGFH0OQHQ== + dependencies: + "@toruslabs/eccrypto" "^4.0.0" + "@toruslabs/tss-lib" "^1.7.1" + bn.js "^5.2.1" + elliptic "^6.5.4" + keccak256 "^1.0.6" + socket.io-client "^4.7.2" + +"@toruslabs/tss-lib@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@toruslabs/tss-lib/-/tss-lib-1.7.1.tgz#758c62b72b41450de5df90acb42d6c2ba5df0482" + integrity sha512-kdjBO95cPq4i7RaRMkjUJFH0aiSHrMZV/A4I42oUr0FkBd7e/RYyn1e1QH1pAAyidCIKbMkwqTxgPg4nuHEcDg== + "@trezor/analytics@1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@trezor/analytics/-/analytics-1.0.8.tgz#e413212fc79b68775d64b70783102d20346efcac" @@ -4611,6 +5123,13 @@ dependencies: "@types/node" "*" +"@types/create-hash@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/create-hash/-/create-hash-1.2.2.tgz#e87247083df8478f6b83655592bde0d709028235" + integrity sha512-Fg8/kfMJObbETFU/Tn+Y0jieYewryLrbKwLCEIwPyklZZVY2qB+64KFjhplGSw+cseZosfFXctXO+PyIYD8iZQ== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.7": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.9.tgz#906996938bc672aaf2fb8c0d3733ae1dda05b005" @@ -5378,7 +5897,7 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@^1.8.0": +"@walletconnect/types@^1.8.0", "@walletconnect/types@~1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== @@ -5570,6 +6089,103 @@ joi "17.9.1" rxjs "^7.5.2" +"@web3auth-mpc/base-provider@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/base-provider/-/base-provider-2.1.9.tgz#407aca075deb981b5cae80567828526aecf385d7" + integrity sha512-i3NmTDk8ezaZXz+wtR1O931DBq9cDfyCzYI/XHFHA0zS0iealCSHXXKD6iACByoUuFDBNSq7GdgZCJNcoqWOGw== + dependencies: + "@toruslabs/base-controllers" "^2.2.6" + "@toruslabs/openlogin-jrpc" "^2.6.0" + "@web3auth-mpc/base" "^2.1.9" + eth-rpc-errors "^4.0.3" + json-rpc-random-id "^1.0.1" + +"@web3auth-mpc/base@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/base/-/base-2.1.9.tgz#1d1768f5b7c955c5a7e8e71a6590aeee49352604" + integrity sha512-T91A+1Vu4C3//5Hj94bhMUaerOu3nVwGfELON2FH16yXNPQsNZPwyBuBoROUYdTp4fOZvCG4OtLiw+g8dPncaA== + dependencies: + "@toruslabs/http-helpers" "^3.2.0" + "@toruslabs/openlogin-jrpc" "^2.6.0" + jwt-decode "^3.1.2" + loglevel "^1.8.0" + ts-custom-error "^3.2.2" + +"@web3auth-mpc/ethereum-provider@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@web3auth-mpc/ethereum-provider/-/ethereum-provider-2.3.0.tgz#000e83696ee6894879a4a8aaef2fd2df5ae3cc6c" + integrity sha512-2RY8GPd2UJ04P4Gzjt+Uec00j721LIceSJoHlwhOPGYNPx0QpRsAmwOmMRNLBA5Y+beKrnQ8b8rGDF9wBUaqtA== + dependencies: + "@ethereumjs/common" "^3.0.0" + "@ethereumjs/rlp" "^4.0.0" + "@ethereumjs/tx" "^4.0.0" + "@ethereumjs/util" "^8.0.0" + "@metamask/eth-sig-util" "^5.0.0" + "@toruslabs/base-controllers" "^2.2.6" + "@toruslabs/http-helpers" "^3.2.0" + "@toruslabs/openlogin-jrpc" "^2.6.0" + "@walletconnect/types" "~1.8.0" + "@web3auth-mpc/base" "^2.1.9" + "@web3auth-mpc/base-provider" "^2.1.9" + assert "^2.0.0" + bignumber.js "^9.1.0" + bn.js "^5.2.1" + eth-rpc-errors "^4.0.3" + jsonschema "^1.4.1" + +"@web3auth/base-provider@^7.0.1": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@web3auth/base-provider/-/base-provider-7.0.4.tgz#a0152ae15585e3f51a7bf5b6f9c1d20f55a52625" + integrity sha512-DDE/K6AP2UBRQqBJUoFggGt7ylQ96pexVnaps9Yw8JOtx573yzVUPs2tYEjhxmAfRGgmDD1J8juokgvCEeKf4g== + dependencies: + "@metamask/rpc-errors" "^6.0.0" + "@toruslabs/base-controllers" "^4.2.0" + "@toruslabs/openlogin-jrpc" "^5.2.0" + "@web3auth/base" "^7.0.4" + json-rpc-random-id "^1.0.1" + +"@web3auth/base@^7.0.1", "@web3auth/base@^7.0.4": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@web3auth/base/-/base-7.0.4.tgz#93a165486ee991938b87a5fa8fe97da7c58a15b3" + integrity sha512-ufpJ0uqOp7k7uGqDS1DohOeIIrsTLUAdyiExM5D+SQYYIXri3M9wbFgyCeAqHbJbfBO6hI9PDcY82QvOGCMI3w== + dependencies: + "@toruslabs/http-helpers" "^5.0.0" + "@toruslabs/openlogin" "^5.2.0" + "@toruslabs/openlogin-jrpc" "^5.2.0" + "@toruslabs/openlogin-utils" "^5.2.0" + jwt-decode "^3.1.2" + loglevel "^1.8.1" + ts-custom-error "^3.3.1" + +"@web3auth/mpc-core-kit@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.1.2.tgz#308f9d441b1275ebcc2c96be8ff976decee6dbcf" + integrity sha512-bx16zYdC3D2KPp5wv55fn6W3RcMGUUbHeoClaDI2czwbUrZyql71A4qQdyi6tMTzy/uAXWZrfB+U4NGk+ec9Pw== + dependencies: + "@tkey-mpc/chrome-storage" "^9.0.2" + "@tkey-mpc/common-types" "^9.0.2" + "@tkey-mpc/core" "^9.0.2" + "@tkey-mpc/security-questions" "^9.0.2" + "@tkey-mpc/service-provider-torus" "^9.0.3" + "@tkey-mpc/share-serialization" "^9.0.2" + "@tkey-mpc/storage-layer-torus" "^9.0.2" + "@toruslabs/constants" "^13.0.1" + "@toruslabs/customauth" "^16.0.6" + "@toruslabs/eccrypto" "4.0.0" + "@toruslabs/fetch-node-details" "^13.0.1" + "@toruslabs/fnd-base" "^13.0.1" + "@toruslabs/metadata-helpers" "^5.x" + "@toruslabs/openlogin-session-manager" "^3.0.0" + "@toruslabs/torus.js" "^11.0.6" + "@toruslabs/tss-client" "^1.7.1" + "@toruslabs/tss-lib" "^1.7.1" + "@web3auth-mpc/ethereum-provider" "^2.3.0" + "@web3auth/base" "^7.0.1" + "@web3auth/base-provider" "^7.0.1" + bn.js "^5.2.1" + bowser "^2.11.0" + elliptic "^6.5.4" + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -5719,6 +6335,13 @@ abitype@0.9.8: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + abortcontroller-polyfill@^1.7.3, abortcontroller-polyfill@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" @@ -6079,6 +6702,13 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -6293,6 +6923,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64url@3.0.1, base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + bchaddrjs@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.5.2.tgz#1f52b5077329774e7c82d4882964628106bb11a0" @@ -6371,6 +7006,13 @@ bip66@^1.1.5: dependencies: safe-buffer "^5.0.1" +biskviit@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/biskviit/-/biskviit-1.0.1.tgz#037a0cd4b71b9e331fd90a1122de17dc49e420a7" + integrity sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w== + dependencies: + psl "^1.1.7" + bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" @@ -6415,7 +7057,7 @@ bn.js@4.11.8: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -7410,7 +8052,7 @@ debug@2.6.9, debug@^2.2.0: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7801,13 +8443,36 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +encoding@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA== + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" + integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + enhanced-resolve@^5.12.0, enhanced-resolve@^5.15.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -8482,7 +9147,7 @@ eth-rpc-errors@^3.0.0: dependencies: fast-safe-stringify "^2.0.6" -eth-rpc-errors@^4.0.2: +eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== @@ -8820,6 +9485,11 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter2@6.4.7: version "6.4.7" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" @@ -9036,7 +9706,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== -fast-safe-stringify@^2.0.6: +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -9074,6 +9744,14 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fetch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fetch/-/fetch-1.1.0.tgz#0a8279f06be37f9f0ebb567560a30a480da59a2e" + integrity sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA== + dependencies: + biskviit "1.0.1" + encoding "0.1.12" + figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -9815,7 +10493,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -11003,6 +11681,11 @@ jsonschema@1.2.2: resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.2.tgz#83ab9c63d65bf4d596f91d81195e78772f6452bc" integrity sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA== +jsonschema@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -11038,6 +11721,20 @@ jsqr@^1.2.0: object.assign "^4.1.4" object.values "^1.1.6" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + +keccak256@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" + integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== + dependencies: + bn.js "^5.2.0" + buffer "^6.0.3" + keccak "^3.0.2" + keccak@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" @@ -11047,7 +11744,7 @@ keccak@3.0.2: node-gyp-build "^4.2.0" readable-stream "^3.6.0" -keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.3: +keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.2, keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== @@ -11314,6 +12011,11 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +loglevel@^1.8.0, loglevel@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -11824,7 +12526,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: +node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -11971,6 +12673,11 @@ object.values@^1.1.6: define-properties "^1.2.0" es-abstract "^1.22.1" +oblivious-set@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" + integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== + oboe@2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/oboe/-/oboe-2.1.5.tgz#5554284c543a2266d7a38f17e073821fbde393cd" @@ -12489,7 +13196,7 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.28, psl@^1.1.33, psl@^1.1.7: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -12843,6 +13550,17 @@ readable-stream@^3.1.1, readable-stream@^3.5.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.2.tgz#e6aced27ad3b9d726d8308515b9a1b98dc1b9d13" + integrity sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-stream@~1.0.15, readable-stream@~1.0.17, readable-stream@~1.0.27-1: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -13468,6 +14186,13 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +serialize-error@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -13622,6 +14347,24 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +socket.io-client@^4.6.1, socket.io-client@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" + integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + socks-proxy-agent@6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" @@ -13854,7 +14597,7 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -14305,6 +15048,11 @@ ts-command-line-args@^2.2.0: command-line-usage "^6.1.0" string-format "^2.0.0" +ts-custom-error@^3.2.2, ts-custom-error@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" + integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== + ts-essentials@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" @@ -14657,6 +15405,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unload@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.4.1.tgz#b0c5b7fb44e17fcbf50dcb8fb53929c59dd226a5" + integrity sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -14970,7 +15723,7 @@ web3-core@1.10.0: web3-core-requestmanager "1.10.0" web3-utils "1.10.0" -web3-core@^1.8.1: +web3-core@1.10.2, web3-core@^1.8.1: version "1.10.2" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.2.tgz#464a15335b3adecc4a1cdd53c89b995769059f03" integrity sha512-qTn2UmtE8tvwMRsC5pXVdHxrQ4uZ6jiLgF5DRUVtdi7dPUmX18Dp9uxKfIfhGcA011EAn8P6+X7r3pvi2YRxBw== @@ -14991,6 +15744,14 @@ web3-eth-abi@1.10.0: "@ethersproject/abi" "^5.6.3" web3-utils "1.10.0" +web3-eth-abi@1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.2.tgz#65db4af1acb0b72cb9d10cd6f045a8bcdb270b1b" + integrity sha512-pY4fQUio7W7ZRSLf+vsYkaxJqaT/jHcALZjIxy+uBQaYAJ3t6zpQqMZkJB3Dw7HUODRJ1yI0NPEFGTnkYf/17A== + dependencies: + "@ethersproject/abi" "^5.6.3" + web3-utils "1.10.2" + web3-eth-accounts@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz#2942beca0a4291455f32cf09de10457a19a48117" @@ -15021,6 +15782,20 @@ web3-eth-contract@1.10.0: web3-eth-abi "1.10.0" web3-utils "1.10.0" +web3-eth-contract@^1.8.1: + version "1.10.2" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.2.tgz#9114c52ba5ca5859f3403abea69a13f8678828ad" + integrity sha512-CZLKPQRmupP/+OZ5A/CBwWWkBiz5B/foOpARz0upMh1yjb0dEud4YzRW2gJaeNu0eGxDLsWVaXhUimJVGYprQw== + dependencies: + "@types/bn.js" "^5.1.1" + web3-core "1.10.2" + web3-core-helpers "1.10.2" + web3-core-method "1.10.2" + web3-core-promievent "1.10.2" + web3-core-subscriptions "1.10.2" + web3-eth-abi "1.10.2" + web3-utils "1.10.2" + web3-eth-ens@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz#96a676524e0b580c87913f557a13ed810cf91cd9" @@ -15222,6 +15997,11 @@ web3@1.10.0: web3-shh "1.10.0" web3-utils "1.10.0" +webextension-polyfill@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" + integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -15712,6 +16492,11 @@ ws@^8.11.0, ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xhr-request-promise@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c" @@ -15752,6 +16537,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From 2747747100ae5919912ddc0ba1932ae2669cc910 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 18 Oct 2023 09:21:23 +0200 Subject: [PATCH 15/52] fix: Add social login feature toggle from config service (#2635) * fix: Add social login feature toggle from config service * fix: Failing tests * fix: Address comments --- .../common/ConnectWallet/MPCLogin.tsx | 38 ++++++++++++---- .../ConnectWallet/__tests__/MPCLogin.test.tsx | 43 ++++++++++++++++--- src/hooks/wallets/consts.ts | 1 + src/hooks/wallets/wallets.ts | 10 +++-- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx index 34ca5b51c4..4573414a9a 100644 --- a/src/components/common/ConnectWallet/MPCLogin.tsx +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -1,6 +1,6 @@ import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' import { Box, Button, SvgIcon, Typography } from '@mui/material' -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import { MpcWalletContext } from './MPCWalletProvider' import { PasswordRecovery } from './PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' @@ -8,18 +8,40 @@ import InfoIcon from '@/public/images/notifications/info.svg' import css from './styles.module.css' import useWallet from '@/hooks/wallets/useWallet' -import { useCurrentChain } from '@/hooks/useChains' -import chains from '@/config/chains' +import useChains, { useCurrentChain } from '@/hooks/useChains' +import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { CGW_NAMES } from '@/hooks/wallets/consts' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { +export const _getSupportedChains = (chains: ChainInfo[]) => { + return chains + .filter((chain) => CGW_NAMES.SOCIAL_LOGIN && !chain.disabledWallets.includes(CGW_NAMES.SOCIAL_LOGIN)) + .map((chainConfig) => chainConfig.chainName) +} +const useGetSupportedChains = () => { + const chains = useChains() + + return useMemo(() => { + return _getSupportedChains(chains.configs) + }, [chains.configs]) +} + +const useIsSocialWalletEnabled = () => { const currentChain = useCurrentChain() + + return isSocialWalletEnabled(currentChain) +} + +const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { const { triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) const wallet = useWallet() const loginPending = walletState === MPCWalletState.AUTHENTICATING - // TODO: Replace with feature flag from config service - const isMPCLoginEnabled = currentChain?.chainId === chains.gno + const supportedChains = useGetSupportedChains() + const isMPCLoginEnabled = useIsSocialWalletEnabled() + const isDisabled = loginPending || !isMPCLoginEnabled const login = async () => { @@ -40,7 +62,7 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { return ( <> - {wallet && userInfo ? ( + {wallet?.label === ONBOARD_MPC_MODULE_LABEL && userInfo ? ( <> - - ) : ( - - )} + + {wallet?.label === ONBOARD_MPC_MODULE_LABEL && userInfo ? ( + + + + ) : ( + + + + )} + {!isMPCLoginEnabled && ( diff --git a/src/components/common/ConnectWallet/PasswordRecovery.tsx b/src/components/common/ConnectWallet/PasswordRecovery.tsx index 17c8fb6060..72ed86a3ac 100644 --- a/src/components/common/ConnectWallet/PasswordRecovery.tsx +++ b/src/components/common/ConnectWallet/PasswordRecovery.tsx @@ -1,3 +1,4 @@ +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { VisibilityOff, Visibility } from '@mui/icons-material' import { DialogContent, @@ -11,6 +12,7 @@ import { } from '@mui/material' import { useState } from 'react' import ModalDialog from '../ModalDialog' +import Track from '../Track' export const PasswordRecovery = ({ recoverFactorWithPassword, @@ -52,10 +54,14 @@ export const PasswordRecovery = ({ control={ setStoreDeviceFactor((prev) => !prev)} />} label="Do not ask again on this device" /> - - + + + diff --git a/src/components/settings/SignerAccountMFA/PasswordForm.tsx b/src/components/settings/SignerAccountMFA/PasswordForm.tsx index f40e2e0522..d891fd850b 100644 --- a/src/components/settings/SignerAccountMFA/PasswordForm.tsx +++ b/src/components/settings/SignerAccountMFA/PasswordForm.tsx @@ -1,4 +1,6 @@ +import Track from '@/components/common/Track' import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { Typography, TextField, Button, Box } from '@mui/material' import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { useState, useMemo } from 'react' @@ -92,14 +94,15 @@ export const PasswordForm = ({ mpcCoreKit }: { mpcCoreKit: Web3AuthMPCCoreKit }) }, })} /> - - + + + ) diff --git a/src/components/settings/SignerAccountMFA/helper.ts b/src/components/settings/SignerAccountMFA/helper.ts index 4c39a02a49..45ff25b062 100644 --- a/src/components/settings/SignerAccountMFA/helper.ts +++ b/src/components/settings/SignerAccountMFA/helper.ts @@ -1,4 +1,6 @@ import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' +import { trackEvent } from '@/services/analytics' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' import { asError } from '@/services/exceptions/utils' @@ -35,6 +37,7 @@ export const enableMFA = async ( } if (!isMFAEnabled(mpcCoreKit)) { + trackEvent(MPC_WALLET_EVENTS.ENABLE_MFA) // 2. enable MFA in mpcCoreKit await mpcCoreKit.enableMFA({}, false) } diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 451bf5d502..cbb49272fc 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -1,5 +1,7 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import Track from '@/components/common/Track' import useWallet from '@/hooks/wallets/useWallet' +import { CREATE_SAFE_EVENTS } from '@/services/analytics' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { Box, Button, Typography } from '@mui/material' import { EthHashInfo } from '@safe-global/safe-react-components' @@ -9,30 +11,43 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const connectWallet = useConnectWallet() const login = async () => { - await connectWallet() - onLogin() + const walletState = await connectWallet() + if (walletState) { + onLogin() + } } if (wallet !== null && wallet?.label !== ONBOARD_MPC_MODULE_LABEL) { return ( - + + + + + ) } diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx index d3dcf4d715..a846dd5f8d 100644 --- a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -41,7 +41,7 @@ describe('WalletLogin', () => { const mockOnLogin = jest.fn() const walletAddress = hexZeroPad('0x1', 20) const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) - jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn()) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn().mockReturnValue([{}])) const result = render() diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 4d08ea716a..24fc232d7d 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -5,7 +5,7 @@ import SafeLogo from '@/public/images/logo-text.svg' import css from './styles.module.css' import { useRouter } from 'next/router' import WalletLogin from './WalletLogin' -import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' +import { LOAD_SAFE_EVENTS, CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' import Track from '@/components/common/Track' import { trackEvent } from '@/services/analytics' @@ -13,7 +13,7 @@ const WelcomeLogin = () => { const router = useRouter() const continueToCreation = () => { - trackEvent(CREATE_SAFE_EVENTS.CREATE_BUTTON) + trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION) router.push(AppRoutes.newSafe.create) } diff --git a/src/hooks/__tests__/useTxTracking.test.ts b/src/hooks/__tests__/useTxTracking.test.ts new file mode 100644 index 0000000000..7526f85bce --- /dev/null +++ b/src/hooks/__tests__/useTxTracking.test.ts @@ -0,0 +1,43 @@ +import * as analytics from '@/services/analytics' +import { WALLET_EVENTS } from '@/services/analytics' +import { txDispatch, TxEvent } from '@/services/tx/txEvents' +import { renderHook } from '@/tests/test-utils' +import { useTxTracking } from '../useTxTracking' + +describe('useTxTracking', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should track relayed executions', () => { + const trackEventSpy = jest.spyOn(analytics, 'trackEvent') + renderHook(() => useTxTracking()) + + txDispatch(TxEvent.RELAYING, { + taskId: '0x123', + groupKey: '0x234', + }) + expect(trackEventSpy).toBeCalledWith({ ...WALLET_EVENTS.RELAYED_EXECUTION, label: undefined }) + }) + + it('should track tx signing', () => { + const trackEventSpy = jest.spyOn(analytics, 'trackEvent') + renderHook(() => useTxTracking()) + + txDispatch(TxEvent.SIGNED, { + txId: '0x123', + }) + expect(trackEventSpy).toBeCalledWith({ ...WALLET_EVENTS.OFF_CHAIN_SIGNATURE, label: undefined }) + }) + + it('should track tx execution', () => { + const trackEventSpy = jest.spyOn(analytics, 'trackEvent') + renderHook(() => useTxTracking()) + + txDispatch(TxEvent.PROCESSING, { + txId: '0x123', + txHash: '0x234', + }) + expect(trackEventSpy).toBeCalledWith({ ...WALLET_EVENTS.ON_CHAIN_INTERACTION, label: undefined }) + }) +}) diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index caa61fe945..e544742283 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -7,6 +7,8 @@ import useOnboard, { connectWallet } from '../useOnboard' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { SecurityQuestionRecovery } from './recovery/SecurityQuestionRecovery' import { DeviceShareRecovery } from './recovery/DeviceShareRecovery' +import { trackEvent } from '@/services/analytics' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' export enum MPCWalletState { NOT_INITIALIZED, @@ -33,7 +35,7 @@ export const useMPCWallet = (): MPCWalletHook => { // This is a critical function that should only be used for testing purposes // Resetting your account means clearing all the metadata associated with it from the metadata server // The key details will be deleted from our server and you will not be able to recover your account - if (!mpcCoreKit || !mpcCoreKit.metadataKey) { + if (!mpcCoreKit?.metadataKey) { throw new Error('MPC Core Kit is not initialized or the user is not logged in') } @@ -73,6 +75,7 @@ export const useMPCWallet = (): MPCWalletHook => { // Check password recovery const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) if (securityQuestions.isEnabled()) { + trackEvent(MPC_WALLET_EVENTS.MANUAL_RECOVERY) setWalletState(MPCWalletState.MANUAL_RECOVERY) return false } diff --git a/src/services/analytics/events/createLoadSafe.ts b/src/services/analytics/events/createLoadSafe.ts index 98689a083d..7017c759bd 100644 --- a/src/services/analytics/events/createLoadSafe.ts +++ b/src/services/analytics/events/createLoadSafe.ts @@ -3,7 +3,12 @@ import { EventType } from '@/services/analytics/types' export const CREATE_SAFE_CATEGORY = 'create-safe' export const CREATE_SAFE_EVENTS = { - CREATE_BUTTON: { + CONTINUE_TO_CREATION: { + action: 'Continue to creation', + category: CREATE_SAFE_CATEGORY, + event: EventType.META, + }, + OPEN_SAFE_CREATION: { action: 'Open stepper', category: CREATE_SAFE_CATEGORY, }, diff --git a/src/services/analytics/events/mpcWallet.ts b/src/services/analytics/events/mpcWallet.ts new file mode 100644 index 0000000000..5f3277c789 --- /dev/null +++ b/src/services/analytics/events/mpcWallet.ts @@ -0,0 +1,31 @@ +import { EventType } from '@/services/analytics/types' + +const MPC_WALLET_CATEGORY = 'mpc-wallet' + +export const MPC_WALLET_EVENTS = { + CONNECT_GOOGLE: { + event: EventType.CLICK, + action: 'Continue with Google button', + category: MPC_WALLET_CATEGORY, + }, + MANUAL_RECOVERY: { + event: EventType.META, + action: 'Account recovery started', + category: MPC_WALLET_CATEGORY, + }, + RECOVER_PASSWORD: { + event: EventType.CLICK, + action: 'Recover account using password', + category: MPC_WALLET_CATEGORY, + }, + UPSERT_PASSWORD: { + event: EventType.CLICK, + action: 'Set or change password', + category: MPC_WALLET_CATEGORY, + }, + ENABLE_MFA: { + event: EventType.META, + action: 'Enable MFA for account', + category: MPC_WALLET_CATEGORY, + }, +} From 1b153d4c2192fd9f635c8a255cd05ee0d4d287d8 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 20 Oct 2023 13:27:37 +0200 Subject: [PATCH 17/52] [Seedless-Onboarding] export account (#2610) --- package.json | 2 +- .../ConnectWallet/MPCWalletProvider.tsx | 1 + .../ExportMPCAccountModal.tsx | 127 ++++++++++++++++++ .../settings/ExportMPCAccount/index.tsx | 27 ++++ .../ExportMPCAccount/styles.module.css | 10 ++ src/hooks/wallets/mpc/useMPCWallet.ts | 20 +++ src/pages/settings/signer-account.tsx | 13 ++ src/services/exceptions/ErrorCodes.ts | 1 + yarn.lock | 2 +- 9 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx create mode 100644 src/components/settings/ExportMPCAccount/index.tsx create mode 100644 src/components/settings/ExportMPCAccount/styles.module.css diff --git a/package.json b/package.json index 33c765ff85..37f312ce33 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.7", - "@web3auth/mpc-core-kit": "^1.1.0", + "@web3auth/mpc-core-kit": "^1.1.2", "blo": "^1.1.1", "bn.js": "^5.2.1", "classnames": "^2.3.1", diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx index a8f0f6fb83..06162aab82 100644 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -8,6 +8,7 @@ export const MpcWalletContext = createContext({ upsertPasswordBackup: () => Promise.resolve(), recoverFactorWithPassword: () => Promise.resolve(false), userInfo: undefined, + exportPk: () => Promise.resolve(undefined), }) export const MpcWalletProvider = ({ children }: { children: ReactElement }) => { diff --git a/src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx b/src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx new file mode 100644 index 0000000000..efffab0b71 --- /dev/null +++ b/src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx @@ -0,0 +1,127 @@ +import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' +import CopyButton from '@/components/common/CopyButton' +import ModalDialog from '@/components/common/ModalDialog' +import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typography } from '@mui/material' +import { useContext, useState } from 'react' +import { useForm } from 'react-hook-form' +import { Visibility, VisibilityOff, Close } from '@mui/icons-material' +import css from './styles.module.css' +import ErrorCodes from '@/services/exceptions/ErrorCodes' +import { logError } from '@/services/exceptions' +import ErrorMessage from '@/components/tx/ErrorMessage' +import { asError } from '@/services/exceptions/utils' + +enum ExportFieldNames { + password = 'password', + pk = 'pk', +} + +type ExportFormData = { + [ExportFieldNames.password]: string + [ExportFieldNames.pk]: string | undefined +} + +const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: boolean }) => { + const { exportPk } = useContext(MpcWalletContext) + const [error, setError] = useState() + + const [showPassword, setShowPassword] = useState(false) + const formMethods = useForm({ + mode: 'all', + defaultValues: { + [ExportFieldNames.password]: '', + }, + }) + const { register, formState, handleSubmit, setValue, watch, reset } = formMethods + + const exportedKey = watch(ExportFieldNames.pk) + + const onSubmit = async (data: ExportFormData) => { + try { + setError(undefined) + const pk = await exportPk(data[ExportFieldNames.password]) + setValue(ExportFieldNames.pk, pk) + } catch (err) { + logError(ErrorCodes._305, err) + setError(asError(err).message) + } + } + + const handleClose = () => { + setError(undefined) + reset() + onClose() + } + return ( + + + + Export your account + + + + + + + +
    + + For security reasons you have to enter your password to reveal your account key. + + {exportedKey ? ( + + + setShowPassword((prev) => !prev)}> + {showPassword ? : } + + + + ), + }} + {...register(ExportFieldNames.pk)} + /> + + ) : ( + <> + + + )} + {error && {error}} + + + + {exportedKey === undefined && ( + + )} + + +
    +
    +
    + ) +} + +export default ExportMPCAccountModal diff --git a/src/components/settings/ExportMPCAccount/index.tsx b/src/components/settings/ExportMPCAccount/index.tsx new file mode 100644 index 0000000000..d8c2b43601 --- /dev/null +++ b/src/components/settings/ExportMPCAccount/index.tsx @@ -0,0 +1,27 @@ +import { Alert, Box, Button, Typography } from '@mui/material' +import { useState } from 'react' +import ExportMPCAccountModal from './ExportMPCAccountModal' + +const ExportMPCAccount = () => { + const [isModalOpen, setIsModalOpen] = useState(false) + + return ( + <> + + + Accounts created via Google can be exported and imported to any non-custodial wallet outside of Safe. + + + Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over + your signer account. + + + + setIsModalOpen(false)} open={isModalOpen} /> + + ) +} + +export default ExportMPCAccount diff --git a/src/components/settings/ExportMPCAccount/styles.module.css b/src/components/settings/ExportMPCAccount/styles.module.css new file mode 100644 index 0000000000..c818925ecd --- /dev/null +++ b/src/components/settings/ExportMPCAccount/styles.module.css @@ -0,0 +1,10 @@ +.close { + position: absolute; + right: var(--space-1); + top: var(--space-1); +} + +.modalError { + width: 100%; + margin: 0; +} diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index e544742283..753b199a48 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -24,6 +24,7 @@ export type MPCWalletHook = { triggerLogin: () => Promise resetAccount: () => Promise userInfo: UserInfo | undefined + exportPk: (password: string) => Promise } export const useMPCWallet = (): MPCWalletHook => { @@ -132,6 +133,24 @@ export const useMPCWallet = (): MPCWalletHook => { return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN } + const exportPk = async (password: string): Promise => { + if (!mpcCoreKit) { + throw new Error('MPC Core Kit is not initialized') + } + const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) + + try { + if (securityQuestions.isEnabled()) { + // Only export PK if recovery works + await securityQuestions.recoverWithPassword(password) + } + const exportedPK = await mpcCoreKit?._UNSAFE_exportTssKey() + return exportedPK + } catch (err) { + throw new Error('Error exporting account. Make sure the password is correct.') + } + } + return { triggerLogin, walletState, @@ -139,5 +158,6 @@ export const useMPCWallet = (): MPCWalletHook => { resetAccount: criticalResetAccount, upsertPasswordBackup: () => Promise.resolve(), userInfo: mpcCoreKit?.state.userInfo, + exportPk, } } diff --git a/src/pages/settings/signer-account.tsx b/src/pages/settings/signer-account.tsx index 3c0f36b91b..13876f2a2d 100644 --- a/src/pages/settings/signer-account.tsx +++ b/src/pages/settings/signer-account.tsx @@ -5,6 +5,7 @@ import Head from 'next/head' import SettingsHeader from '@/components/settings/SettingsHeader' import SignerAccountMFA from '@/components/settings/SignerAccountMFA' +import ExportMPCAccount from '@/components/settings/ExportMPCAccount' const SignerAccountPage: NextPage = () => { return ( @@ -29,6 +30,18 @@ const SignerAccountPage: NextPage = () => {
    + + + + + Account export + + + + + + + ) diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 96d8aab23c..38cc0c27a0 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -17,6 +17,7 @@ enum ErrorCodes { _302 = '302: Error connecting to the wallet', _303 = '303: Error creating pairing session', _304 = '304: Error enabling MFA', + _305 = '305: Error exporting account key', _400 = '400: Error requesting browser notification permissions', _401 = '401: Error tracking push notifications', diff --git a/yarn.lock b/yarn.lock index f19a92c25a..ce6ac8751b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6157,7 +6157,7 @@ loglevel "^1.8.1" ts-custom-error "^3.3.1" -"@web3auth/mpc-core-kit@^1.1.0": +"@web3auth/mpc-core-kit@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.1.2.tgz#308f9d441b1275ebcc2c96be8ff976decee6dbcf" integrity sha512-bx16zYdC3D2KPp5wv55fn6W3RcMGUUbHeoClaDI2czwbUrZyql71A4qQdyi6tMTzy/uAXWZrfB+U4NGk+ec9Pw== From 0140ec0f4a171a9d0d97bb34f19a15376d379943 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:31:22 +0200 Subject: [PATCH 18/52] [Seedless Onboarding]: Settings password form (#2644) * feat: Implement new form layout * fix: Add password strength check, add cancel button * fix: Use RHF isSubmitting flag, move validate logic into onChange * fix: Update form dynamically when connecting/disconnecting, show notifications on success/error * fix: Update password match when typing in a new password * fix: Allow medium and strong passwords * fix: reset form state, fix accordion expand * fix: simplify validation, extract regex logic and test * refactor: Extract isSocialLoginWallet * fix: Failing test * fix: Remove ts-ignore * fix: Dont reset form fields on error --- public/images/common/bar-chart.svg | 5 + public/images/common/lock-warning.svg | 6 + public/images/common/shield-off.svg | 12 + public/images/common/shield.svg | 3 + .../common/ConnectWallet/AccountCenter.tsx | 6 +- .../common/ConnectWallet/MPCLogin.tsx | 8 +- .../common/NetworkSelector/index.tsx | 6 +- src/components/common/WalletInfo/index.tsx | 4 +- src/components/new-safe/create/index.tsx | 5 +- .../create/steps/ReviewStep/index.tsx | 6 +- .../SignerAccountMFA/PasswordForm.test.tsx | 45 +++ .../SignerAccountMFA/PasswordForm.tsx | 285 +++++++++++++----- .../SignerAccountMFA/PasswordInput.tsx | 44 +++ .../settings/SignerAccountMFA/helper.ts | 20 ++ .../settings/SignerAccountMFA/index.tsx | 12 +- .../SignerAccountMFA/styles.module.css | 63 ++++ .../welcome/WelcomeLogin/WalletLogin.tsx | 6 +- .../__tests__/WalletLogin.test.tsx | 2 + src/services/mpc/module.ts | 4 + 19 files changed, 450 insertions(+), 92 deletions(-) create mode 100644 public/images/common/bar-chart.svg create mode 100644 public/images/common/lock-warning.svg create mode 100644 public/images/common/shield-off.svg create mode 100644 public/images/common/shield.svg create mode 100644 src/components/settings/SignerAccountMFA/PasswordForm.test.tsx create mode 100644 src/components/settings/SignerAccountMFA/PasswordInput.tsx create mode 100644 src/components/settings/SignerAccountMFA/styles.module.css diff --git a/public/images/common/bar-chart.svg b/public/images/common/bar-chart.svg new file mode 100644 index 0000000000..4c2ac5f584 --- /dev/null +++ b/public/images/common/bar-chart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/common/lock-warning.svg b/public/images/common/lock-warning.svg new file mode 100644 index 0000000000..6972e1bb15 --- /dev/null +++ b/public/images/common/lock-warning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/common/shield-off.svg b/public/images/common/shield-off.svg new file mode 100644 index 0000000000..f36482d5ca --- /dev/null +++ b/public/images/common/shield-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/common/shield.svg b/public/images/common/shield.svg new file mode 100644 index 0000000000..f7d12a900f --- /dev/null +++ b/public/images/common/shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index a4208e8af1..6ff571733f 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -13,7 +13,7 @@ import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import WalletInfo from '../WalletInfo' import ChainIndicator from '@/components/common/ChainIndicator' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { isSocialLoginWallet } from '@/services/mpc/module' import SocialLoginInfo from '@/components/common/SocialLoginInfo' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { @@ -51,6 +51,8 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const open = Boolean(anchorEl) const id = open ? 'simple-popover' : undefined + const isSocialLogin = isSocialLoginWallet(wallet.label) + return ( <> @@ -82,7 +84,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { - {wallet.label === ONBOARD_MPC_MODULE_LABEL ? ( + {isSocialLogin ? ( ) : ( void }) => { } } + const isSocialLogin = isSocialLoginWallet(wallet?.label) + return ( <> - {wallet?.label === ONBOARD_MPC_MODULE_LABEL && userInfo ? ( - + {isSocialLogin && userInfo ? ( + diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts b/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts index 21dc6f9cb5..94b88cb8f0 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts @@ -5,8 +5,6 @@ import { logError } from '@/services/exceptions' import ErrorCodes from '@/services/exceptions/ErrorCodes' import { asError } from '@/services/exceptions/utils' import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import { showNotification } from '@/store/notificationsSlice' -import { type AppDispatch } from '@/store' export const isMFAEnabled = (mpcCoreKit: Web3AuthMPCCoreKit) => { if (!mpcCoreKit) { @@ -17,14 +15,13 @@ export const isMFAEnabled = (mpcCoreKit: Web3AuthMPCCoreKit) => { } export const enableMFA = async ( - dispatch: AppDispatch, mpcCoreKit: Web3AuthMPCCoreKit, { newPassword, - oldPassword, + currentPassword, }: { newPassword: string - oldPassword: string | undefined + currentPassword: string | undefined }, ) => { if (!mpcCoreKit) { @@ -33,7 +30,7 @@ export const enableMFA = async ( const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) try { // 1. setup device factor with password recovery - await securityQuestions.upsertPassword(newPassword, oldPassword) + await securityQuestions.upsertPassword(newPassword, currentPassword) const securityQuestionFactor = await securityQuestions.recoverWithPassword(newPassword) if (!securityQuestionFactor) { throw Error('Could not recover using the new password recovery') @@ -46,25 +43,9 @@ export const enableMFA = async ( } await mpcCoreKit.commitChanges() - - dispatch( - showNotification({ - variant: 'success', - groupKey: 'global-upsert-password', - message: 'Successfully created or updated password', - }), - ) } catch (e) { const error = asError(e) logError(ErrorCodes._304, error.message) - - // TODO: Check if we should use a notification or show an error inside the form - dispatch( - showNotification({ - variant: 'error', - groupKey: 'global-upsert-password', - message: 'Failed to create or update password', - }), - ) + throw error } } diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx index 43a3545b84..5121193345 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx @@ -11,6 +11,7 @@ import { FormControl, SvgIcon, Divider, + Alert, } from '@mui/material' import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { useState, useMemo, type ChangeEvent } from 'react' @@ -24,17 +25,16 @@ import BarChartIcon from '@/public/images/common/bar-chart.svg' import ShieldIcon from '@/public/images/common/shield.svg' import ShieldOffIcon from '@/public/images/common/shield-off.svg' import useMPC from '@/hooks/wallets/mpc/useMPC' -import { useAppDispatch } from '@/store' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' enum PasswordFieldNames { - oldPassword = 'oldPassword', + currentPassword = 'currentPassword', newPassword = 'newPassword', confirmPassword = 'confirmPassword', } type PasswordFormData = { - [PasswordFieldNames.oldPassword]: string | undefined + [PasswordFieldNames.currentPassword]: string | undefined [PasswordFieldNames.newPassword]: string [PasswordFieldNames.confirmPassword]: string } @@ -50,7 +50,9 @@ const strongPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za- // At least 9 characters, one lowercase, one uppercase, one number, one symbol const mediumPassword = new RegExp('((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{9,}))') -export const _getPasswordStrength = (value: string): PasswordStrength => { +export const _getPasswordStrength = (value: string): PasswordStrength | undefined => { + if (value === '') return undefined + if (strongPassword.test(value)) { return PasswordStrength.strong } @@ -78,15 +80,16 @@ const passwordStrengthMap = { } as const const SocialSignerMFA = () => { - const dispatch = useAppDispatch() const mpcCoreKit = useMPC() - const [passwordStrength, setPasswordStrength] = useState(PasswordStrength.weak) + const [passwordStrength, setPasswordStrength] = useState() + const [submitError, setSubmitError] = useState() + const [open, setOpen] = useState(false) const formMethods = useForm({ mode: 'all', defaultValues: { [PasswordFieldNames.confirmPassword]: '', - [PasswordFieldNames.oldPassword]: undefined, + [PasswordFieldNames.currentPassword]: undefined, [PasswordFieldNames.newPassword]: '', }, }) @@ -103,16 +106,29 @@ const SocialSignerMFA = () => { const onSubmit = async (data: PasswordFormData) => { if (!mpcCoreKit) return - await enableMFA(dispatch, mpcCoreKit, data) + try { + await enableMFA(mpcCoreKit, data) + onReset() + setOpen(false) + } catch (e) { + setSubmitError('The password you entered is incorrect. Please try again.') + } } const onReset = () => { reset() - setPasswordStrength(PasswordStrength.weak) + setPasswordStrength(undefined) + setSubmitError(undefined) + } + + const toggleAccordion = () => { + setOpen((prev) => !prev) } const confirmPassword = watch(PasswordFieldNames.confirmPassword) - const passwordsMatch = watch(PasswordFieldNames.newPassword) === confirmPassword && confirmPassword !== '' + const newPassword = watch(PasswordFieldNames.newPassword) + const passwordsEmpty = confirmPassword === '' && newPassword === '' + const passwordsMatch = newPassword === confirmPassword const isSubmitDisabled = !passwordsMatch || @@ -128,7 +144,7 @@ const SocialSignerMFA = () => { Protect your social login signer with a password. It will be used to restore access in another browser or on another device. - + }> @@ -140,13 +156,12 @@ const SocialSignerMFA = () => { {isPasswordSet && ( <> - You already have a password setup. @@ -174,10 +189,16 @@ const SocialSignerMFA = () => { alignItems="center" gap={1} mt={1} - className={css[passwordStrengthMap[passwordStrength].className]} + className={ + passwordStrength !== undefined + ? css[passwordStrengthMap[passwordStrength].className] + : css.defaultPassword + } > - {passwordStrengthMap[passwordStrength].label} password + {passwordStrength !== undefined + ? `${passwordStrengthMap[passwordStrength].label} password` + : 'Password strength'} Include at least 9 or more characters, a number, an uppercase letter and a symbol @@ -187,8 +208,8 @@ const SocialSignerMFA = () => { @@ -198,9 +219,19 @@ const SocialSignerMFA = () => { alignItems="center" gap={1} mt={1} - className={passwordsMatch ? css.passwordsMatch : css.passwordsNoMatch} + className={ + passwordsEmpty + ? css.passwordsShouldMatch + : passwordsMatch + ? css.passwordsMatch + : css.passwordsNoMatch + } > - {passwordsMatch ? ( + {passwordsEmpty ? ( + <> + Passwords should match + + ) : passwordsMatch ? ( <> Passwords match @@ -212,19 +243,18 @@ const SocialSignerMFA = () => { - - - - + + + +
    diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css index 72742ceeba..e753abc7be 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css @@ -29,6 +29,19 @@ margin-bottom: 0; } +.defaultPassword, +.passwordsShouldMatch { + color: var(--color-border-main); +} + +.passwordsShouldMatch svg path { + stroke: var(--color-border-main); +} + +.defaultPassword svg path { + stroke: var(--color-border-main); +} + .weakPassword { color: var(--color-error-main); } diff --git a/src/services/analytics/events/createLoadSafe.ts b/src/services/analytics/events/createLoadSafe.ts index 7017c759bd..b95a93587f 100644 --- a/src/services/analytics/events/createLoadSafe.ts +++ b/src/services/analytics/events/createLoadSafe.ts @@ -42,6 +42,10 @@ export const CREATE_SAFE_EVENTS = { action: 'Retry Safe creation', category: CREATE_SAFE_CATEGORY, }, + CANCEL_CREATE_SAFE_FORM: { + action: 'Cancel safe creation form', + category: CREATE_SAFE_CATEGORY, + }, CANCEL_CREATE_SAFE: { event: EventType.META, action: 'Cancel Safe creation', diff --git a/src/styles/globals.css b/src/styles/globals.css index 1734b5b58e..91c240e053 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -70,6 +70,10 @@ input[type='number'] { stroke: var(--color-border-main); } +.illustration-very-light-stroke { + stroke: var(--color-border-light); +} + .illustration-background-stroke { stroke: var(--color-logo-background); } From 672523f762c55c3f52b0d3c3c2a36516b79ce46b Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:20:17 +0200 Subject: [PATCH 25/52] fix: Safe creation e2e test for seedless onboarding (#2694) * fix: Safe creation e2e test for seedless onboarding * fix: Extend e2e test, preserve query params when navigating the user --- cypress/e2e/pages/create_wallet.pages.js | 17 +++++-- cypress/e2e/smoke/create_safe_simple.cy.js | 51 ++++++++++--------- .../__tests__/useSyncSafeCreationStep.test.ts | 2 +- .../create/useSyncSafeCreationStep.ts | 2 +- src/components/welcome/WelcomeLogin/index.tsx | 4 +- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 65ca64c824..4545f8dc29 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -7,16 +7,17 @@ const ownerAddress = 'input[name^="owners"][name$="address"]' const thresholdInput = 'input[name="threshold"]' export const removeOwnerBtn = 'button[aria-label="Remove owner"]' const connectingContainer = 'div[class*="connecting-container"]' -const createNewSafeBtn = 'span[data-track="create-safe: Open stepper"]' +const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' +const connectWalletBtn = 'Connect wallet' const changeNetworkWarningStr = 'Change your wallet network' const safeAccountSetupStr = 'Safe Account setup' -const policy1_1 = '1/1 policy' +const policy1_2 = '1/2 policy' export const walletName = 'test1-sepolia-safe' export const defaltSepoliaPlaceholder = 'sepolia-safe' -export function verifyPolicy1_1() { - cy.contains(policy1_1).should('exist') +export function verifyPolicy1_2() { + cy.contains(policy1_2).should('exist') // TOD: Need data-cy for containers } @@ -49,10 +50,18 @@ export function clickOnCreateNewSafeBtn() { cy.get(createNewSafeBtn).click().wait(1000) } +export function clickOnConnectWalletAndCreateBtn() { + cy.contains('[data-testid="welcome-login"]', connectWalletBtn).click().wait(1000) +} + export function typeWalletName(name) { cy.get(nameInput).type(name).should('have.value', name) } +export function clearWalletName() { + cy.get(nameInput).clear() +} + export function selectNetwork(network, regex = false) { cy.wait(1000) cy.get(selectNetworkBtn).should('exist').click() diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index a135d89ddb..7f602b3c8a 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -5,23 +5,25 @@ import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' describe('Create Safe tests', () => { + before(() => { + cy.visit(constants.welcomeUrl + '?chain=sep') + }) beforeEach(() => { - cy.visit(constants.createNewSafeSepoliaUrl) cy.clearLocalStorage() main.acceptCookies() }) - it('C55742: Verify a Wallet can be connected', () => { - owner.waitForConnectionStatus() - cy.visit(constants.welcomeUrl) + it.only('C55742: Verify a Wallet can be connected', () => { + createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnConnectBtn() + cy.url().should('include', constants.welcomeUrl) + createwallet.clickOnConnectWalletAndCreateBtn() createwallet.connectWallet() + cy.url().should('include', constants.createNewSafeSepoliaUrl) }) - it('C55743: Verify Next button is disabled until switching to network is done', () => { + it.only('C55743: Verify Next button is disabled until switching to network is done', () => { owner.waitForConnectionStatus() createwallet.selectNetwork(constants.networks.ethereum) createwallet.checkNetworkChangeWarningMsg() @@ -30,15 +32,16 @@ describe('Create Safe tests', () => { createwallet.verifyNextBtnIsEnabled() }) - it('C32378: Verify that a new Wallet has default name related to the selected network', () => { + it.only('C32378: Verify that a new Wallet has default name related to the selected network', () => { owner.waitForConnectionStatus() createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) }) - it('C4790: Verify error message is displayed if wallet name input exceeds 50 characters', () => { + it.only('C4790: Verify error message is displayed if wallet name input exceeds 50 characters', () => { owner.waitForConnectionStatus() createwallet.typeWalletName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + createwallet.clearWalletName() }) it('C55744: Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { @@ -55,21 +58,18 @@ describe('Create Safe tests', () => { it('C4791: Verify error message is displayed if owner name input exceeds 50 characters', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() owner.typeExistingOwnerName(0, main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) }) it('C55745: Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() owner.typeExistingOwnerName(0, main.generateRandomString(50)) owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) }) it('C55746: Verify Add and Remove Owner Row works as expected', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(2) owner.verifyExistingOwnerAddress(1, '') @@ -82,26 +82,28 @@ describe('Create Safe tests', () => { it('C55748: Verify Threshold Setup', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() - owner.verifyNumberOfOwners(2) createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(3) - owner.verifyThresholdLimit(1, 3) + createwallet.clickOnAddNewOwnerBtn() + owner.verifyNumberOfOwners(4) + owner.verifyThresholdLimit(1, 4) createwallet.updateThreshold(3) createwallet.removeOwner(1) + owner.verifyThresholdLimit(1, 3) + createwallet.removeOwner(1) owner.verifyThresholdLimit(1, 2) + createwallet.updateThreshold(1) }) it('C55749: Verify data persistence', () => { - const ownerName = 'David' owner.waitForConnectionStatus() - createwallet.typeWalletName(createwallet.walletName) - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() + const ownerName = 'David' createwallet.typeOwnerName(ownerName, 1) createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) - owner.verifyThresholdLimit(1, 2) + owner.clickOnBackBtn() + createwallet.clearWalletName() + createwallet.typeWalletName(createwallet.walletName) + owner.clickOnNextBtn() owner.clickOnNextBtn() createwallet.verifySafeNameInSummaryStep(createwallet.walletName) createwallet.verifyOwnerNameInSummaryStep(ownerName) @@ -123,15 +125,14 @@ describe('Create Safe tests', () => { createwallet.verifyEstimatedFeeInSummaryStep() }) - it('C55750: Verify tip is displayed on right side for threshold 1/1', () => { + it('C55750: Verify tip is displayed on right side for threshold 1/2', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() - createwallet.verifyPolicy1_1() + owner.clickOnBackBtn() + createwallet.verifyPolicy1_2() }) it('C55747: Verify address input validation rules', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.typeOwnerAddress(main.generateRandomString(10), 1) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index a3bc08f3da..310b09fbf7 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -36,7 +36,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) expect(mockSetStep).not.toHaveBeenCalled() - expect(mockPushRoute).toHaveBeenCalledWith(AppRoutes.welcome) + expect(mockPushRoute).toHaveBeenCalledWith({ pathname: AppRoutes.welcome, query: undefined }) }) it('should go to the fourth step if there is a pending safe', async () => { diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 3033daf932..9604114671 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -22,7 +22,7 @@ const useSyncSafeCreationStep = (setStep: StepRenderProps['setS // Jump to connect wallet step if there is no wallet and no pending Safe if (!wallet) { - router.push(AppRoutes.welcome) + router.push({ pathname: AppRoutes.welcome, query: router.query }) } // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 24fc232d7d..998d660307 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -14,11 +14,11 @@ const WelcomeLogin = () => { const continueToCreation = () => { trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION) - router.push(AppRoutes.newSafe.create) + router.push({ pathname: AppRoutes.newSafe.create, query: router.query }) } return ( - + From abdc544e22e9839ab01d251ab5cb6f6969323e20 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 30 Oct 2023 12:58:20 +0100 Subject: [PATCH 26/52] [Seedless-Onboarding] Refactor context + provider into service class (#2695) Co-authored-by: Usame Algan --- .../ConnectWallet/MPCWalletProvider.tsx | 20 - .../common/ConnectWallet/WalletDetails.tsx | 4 +- .../ConnectWallet/__tests__/MPCLogin.test.tsx | 136 ------- .../common/ConnectWallet/styles.module.css | 5 + .../common/NetworkSelector/index.tsx | 2 +- .../common/SocialLoginInfo/index.tsx | 6 +- .../PasswordRecovery.tsx | 16 +- .../__tests__/PasswordRecovery.test.tsx | 48 +++ .../__tests__/SocialSignerLogin.test.tsx | 179 +++++++++ .../MPCLogin.tsx => SocialSigner/index.tsx} | 85 +++-- .../common/SocialSigner/styles.module.css | 24 ++ .../common/WalletInfo/index.test.tsx | 38 +- src/components/common/WalletInfo/index.tsx | 23 +- .../common/WalletOverview/index.tsx | 2 +- src/components/new-safe/create/index.tsx | 2 +- .../create/steps/ReviewStep/index.test.tsx | 2 +- .../create/steps/ReviewStep/index.tsx | 2 +- .../ExportMPCAccountModal.tsx | 11 +- .../SocialSignerExport/index.tsx | 21 +- .../SecurityLogin/SocialSignerMFA/helper.ts | 51 --- .../SecurityLogin/SocialSignerMFA/index.tsx | 23 +- .../settings/SecurityLogin/index.tsx | 2 +- .../welcome/WelcomeLogin/WalletLogin.tsx | 2 +- src/components/welcome/WelcomeLogin/index.tsx | 4 +- .../wallets/mpc/__tests__/useMPC.test.ts | 4 +- .../mpc/__tests__/useMPCWallet.test.ts | 345 ------------------ src/hooks/wallets/mpc/useMPC.ts | 4 +- src/hooks/wallets/mpc/useMPCWallet.ts | 185 ---------- src/hooks/wallets/mpc/useSocialWallet.ts | 63 ++++ src/hooks/wallets/wallets.ts | 2 +- src/pages/_app.tsx | 5 +- .../mpc/{module.ts => SocialLoginModule.ts} | 0 src/services/mpc/SocialWalletService.ts | 153 ++++++++ .../mpc/__mocks__/SocialWalletService.ts | 74 ++++ .../mpc/__tests__/SocialWalletService.test.ts | 269 ++++++++++++++ src/services/mpc/__tests__/module.test.ts | 2 +- src/services/mpc/interfaces.ts | 52 +++ .../mpc/recovery/DeviceShareRecovery.ts | 13 +- .../mpc/recovery/SecurityQuestionRecovery.ts | 0 src/tests/builders/eip1193Provider.ts | 10 + src/tests/builders/wallet.ts | 16 + 41 files changed, 1055 insertions(+), 850 deletions(-) delete mode 100644 src/components/common/ConnectWallet/MPCWalletProvider.tsx delete mode 100644 src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx rename src/components/common/{ConnectWallet => SocialSigner}/PasswordRecovery.tsx (87%) create mode 100644 src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx create mode 100644 src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx rename src/components/common/{ConnectWallet/MPCLogin.tsx => SocialSigner/index.tsx} (71%) create mode 100644 src/components/common/SocialSigner/styles.module.css delete mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts delete mode 100644 src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts delete mode 100644 src/hooks/wallets/mpc/useMPCWallet.ts create mode 100644 src/hooks/wallets/mpc/useSocialWallet.ts rename src/services/mpc/{module.ts => SocialLoginModule.ts} (100%) create mode 100644 src/services/mpc/SocialWalletService.ts create mode 100644 src/services/mpc/__mocks__/SocialWalletService.ts create mode 100644 src/services/mpc/__tests__/SocialWalletService.test.ts create mode 100644 src/services/mpc/interfaces.ts rename src/{hooks/wallets => services}/mpc/recovery/DeviceShareRecovery.ts (79%) rename src/{hooks/wallets => services}/mpc/recovery/SecurityQuestionRecovery.ts (100%) create mode 100644 src/tests/builders/eip1193Provider.ts create mode 100644 src/tests/builders/wallet.ts diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx deleted file mode 100644 index 27872ceddc..0000000000 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { type MPCWalletHook, MPCWalletState, useMPCWallet } from '@/hooks/wallets/mpc/useMPCWallet' -import { createContext, type ReactElement } from 'react' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' - -export const MpcWalletContext = createContext({ - walletState: MPCWalletState.NOT_INITIALIZED, - setWalletState: () => {}, - triggerLogin: () => Promise.resolve(COREKIT_STATUS.NOT_INITIALIZED), - resetAccount: () => Promise.resolve(), - upsertPasswordBackup: () => Promise.resolve(), - recoverFactorWithPassword: () => Promise.resolve(false), - userInfo: undefined, - exportPk: () => Promise.resolve(undefined), -}) - -export const MpcWalletProvider = ({ children }: { children: ReactElement }) => { - const mpcValue = useMPCWallet() - - return {children} -} diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 29c09582a4..3421e0f8a7 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -2,7 +2,7 @@ import { Divider, Typography } from '@mui/material' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -import MPCLogin from './MPCLogin' +import SocialSigner from '@/components/common/SocialSigner' import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { @@ -18,7 +18,7 @@ const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement = - + ) } diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx deleted file mode 100644 index 249e8da820..0000000000 --- a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { render, waitFor } from '@/tests/test-utils' -import * as useWallet from '@/hooks/wallets/useWallet' -import * as useMPCWallet from '@/hooks/wallets/mpc/useMPCWallet' -import * as chains from '@/hooks/useChains' - -import MPCLogin, { _getSupportedChains } from '../MPCLogin' -import { hexZeroPad } from '@ethersproject/bytes' -import { type EIP1193Provider } from '@web3-onboard/common' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' -import { MpcWalletProvider } from '../MPCWalletProvider' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' - -describe('MPCLogin', () => { - beforeEach(() => { - jest.resetAllMocks() - }) - - it('should render continue with connected account when on gnosis chain', async () => { - const mockOnLogin = jest.fn() - const walletAddress = hexZeroPad('0x1', 20) - jest - .spyOn(chains, 'useCurrentChain') - .mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo) - jest.spyOn(useWallet, 'default').mockReturnValue({ - address: walletAddress, - chainId: '5', - label: ONBOARD_MPC_MODULE_LABEL, - provider: {} as unknown as EIP1193Provider, - }) - jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ - userInfo: { - email: 'test@safe.test', - name: 'Test Testermann', - profileImage: 'test.png', - }, - triggerLogin: jest.fn(), - walletState: useMPCWallet.MPCWalletState.READY, - } as unknown as useMPCWallet.MPCWalletHook) - - const result = render( - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => { - const mockOnLogin = jest.fn() - jest - .spyOn(chains, 'useCurrentChain') - .mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo) - jest.spyOn(useWallet, 'default').mockReturnValue(null) - const mockTriggerLogin = jest.fn(() => COREKIT_STATUS.LOGGED_IN) - jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ - triggerLogin: mockTriggerLogin, - } as unknown as useMPCWallet.MPCWalletHook) - - const result = render( - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - button.click() - - await waitFor(() => { - expect(mockOnLogin).toHaveBeenCalled() - }) - }) - - it('should disable the Google Login button with a message when not on gnosis chain', async () => { - const mockEthereumChain = { chainId: '1', chainName: 'Ethereum', disabledWallets: ['socialLogin'] } as ChainInfo - const mockGoerliChain = { chainId: '5', chainName: 'Goerli', disabledWallets: ['TallyHo'] } as ChainInfo - - jest - .spyOn(chains, 'useCurrentChain') - .mockReturnValue({ chainId: '1', disabledWallets: ['socialLogin'] } as unknown as ChainInfo) - jest.spyOn(chains, 'default').mockReturnValue({ configs: [mockEthereumChain, mockGoerliChain] }) - jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ - triggerLogin: jest.fn(), - } as unknown as useMPCWallet.MPCWalletHook) - - const result = render( - - - , - ) - - expect(result.getByText('Currently only supported on Goerli')).toBeInTheDocument() - expect(await result.findByRole('button')).toBeDisabled() - }) - - describe('getSupportedChains', () => { - it('returns chain names where social login is enabled', () => { - const mockEthereumChain = { chainId: '1', chainName: 'Ethereum', disabledWallets: ['socialLogin'] } as ChainInfo - const mockGnosisChain = { chainId: '100', chainName: 'Gnosis Chain', disabledWallets: ['Coinbase'] } as ChainInfo - const mockGoerliChain = { chainId: '5', chainName: 'Goerli', disabledWallets: [] } as unknown as ChainInfo - - const mockChains = [mockEthereumChain, mockGnosisChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual(['Gnosis Chain', 'Goerli']) - }) - - it('returns an empty array if social login is not enabled on any chain', () => { - const mockEthereumChain = { chainId: '1', chainName: 'Ethereum', disabledWallets: ['socialLogin'] } as ChainInfo - const mockGoerliChain = { chainId: '5', chainName: 'Goerli', disabledWallets: ['socialLogin'] } as ChainInfo - - const mockChains = [mockEthereumChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual([]) - }) - }) -}) diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index afa1289886..dd7bf01c3a 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -70,6 +70,11 @@ min-height: 42px; } +.loginError { + width: 100%; + margin: 0; +} + @media (max-width: 599.95px) { .socialLoginInfo > div > div { display: none; diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 9b2c7adaaa..4588d4d403 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -13,7 +13,7 @@ import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import useWallet from '@/hooks/wallets/useWallet' import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' -import { isSocialLoginWallet } from '@/services/mpc/module' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' const keepPathRoutes = [AppRoutes.welcome, AppRoutes.newSafe.create, AppRoutes.newSafe.load] diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx index 8615a8a3be..4329cc2c83 100644 --- a/src/components/common/SocialLoginInfo/index.tsx +++ b/src/components/common/SocialLoginInfo/index.tsx @@ -1,14 +1,13 @@ import { Box, Typography } from '@mui/material' import css from './styles.module.css' -import { useContext } from 'react' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type ConnectedWallet } from '@/services/onboard' -import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' import CopyAddressButton from '@/components/common/CopyAddressButton' import ExplorerButton from '@/components/common/ExplorerButton' import { getBlockExplorerLink } from '@/utils/chains' import { useAppSelector } from '@/store' import { selectSettings } from '@/store/settingsSlice' +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' const SocialLoginInfo = ({ wallet, @@ -19,7 +18,8 @@ const SocialLoginInfo = ({ chainInfo?: ChainInfo hideActions?: boolean }) => { - const { userInfo } = useContext(MpcWalletContext) + const socialWalletService = useSocialWallet() + const userInfo = socialWalletService?.getUserInfo() const prefix = chainInfo?.shortName const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined const settings = useAppSelector(selectSettings) diff --git a/src/components/common/ConnectWallet/PasswordRecovery.tsx b/src/components/common/SocialSigner/PasswordRecovery.tsx similarity index 87% rename from src/components/common/ConnectWallet/PasswordRecovery.tsx rename to src/components/common/SocialSigner/PasswordRecovery.tsx index d5c47fff6c..5a40e95756 100644 --- a/src/components/common/ConnectWallet/PasswordRecovery.tsx +++ b/src/components/common/SocialSigner/PasswordRecovery.tsx @@ -11,9 +11,12 @@ import { FormControl, } from '@mui/material' import { useState } from 'react' -import Track from '../Track' +import Track from '@/components/common/Track' import { FormProvider, useForm } from 'react-hook-form' import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' +import ErrorMessage from '@/components/tx/ErrorMessage' + +import css from './styles.module.css' type PasswordFormData = { password: string @@ -35,14 +38,17 @@ export const PasswordRecovery = ({ }, }) - const { handleSubmit, formState, setError } = formMethods + const { handleSubmit, formState } = formMethods + + const [error, setError] = useState() const onSubmit = async (data: PasswordFormData) => { + setError(undefined) try { await recoverFactorWithPassword(data.password, storeDeviceFactor) onSuccess?.() } catch (e) { - setError('password', { type: 'custom', message: 'Incorrect password' }) + setError('Incorrect password') } } @@ -71,7 +77,7 @@ export const PasswordRecovery = ({ - + + {error && {error}} + diff --git a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx b/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx new file mode 100644 index 0000000000..488a33cccf --- /dev/null +++ b/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, render } from '@/tests/test-utils' +import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' +import { act, waitFor } from '@testing-library/react' + +describe('PasswordRecovery', () => { + it('displays an error if password is wrong', async () => { + const mockRecoverWithPassword = jest.fn(() => Promise.reject()) + const mockOnSuccess = jest.fn() + + const { getByText, getByLabelText } = render( + , + ) + + const passwordField = getByLabelText('Recovery password') + const submitButton = getByText('Submit') + + act(() => { + fireEvent.change(passwordField, { target: { value: 'somethingwrong' } }) + submitButton.click() + }) + + await waitFor(() => { + expect(mockOnSuccess).not.toHaveBeenCalled() + expect(getByText('Incorrect password')).toBeInTheDocument() + }) + }) + + it('calls onSuccess if password is correct', async () => { + const mockRecoverWithPassword = jest.fn(() => Promise.resolve()) + const mockOnSuccess = jest.fn() + + const { getByText, getByLabelText } = render( + , + ) + + const passwordField = getByLabelText('Recovery password') + const submitButton = getByText('Submit') + + act(() => { + fireEvent.change(passwordField, { target: { value: 'somethingCorrect' } }) + submitButton.click() + }) + + await waitFor(() => { + expect(mockOnSuccess).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx new file mode 100644 index 0000000000..29a8ab560d --- /dev/null +++ b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx @@ -0,0 +1,179 @@ +import { act, render, waitFor } from '@/tests/test-utils' + +import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import SocialWalletService from '@/services/mpc/SocialWalletService' +import { TxModalProvider } from '@/components/tx-flow' +import { fireEvent } from '@testing-library/react' +import { type ISocialWalletService } from '@/services/mpc/interfaces' +import { connectedWalletBuilder } from '@/tests/builders/wallet' +import { chainBuilder } from '@/tests/builders/chains' + +jest.mock('@/services/mpc/SocialWalletService') + +const mockWallet = connectedWalletBuilder().with({ chainId: '5', label: ONBOARD_MPC_MODULE_LABEL }).build() + +describe('SocialSignerLogin', () => { + let mockSocialWalletService: ISocialWalletService + + beforeEach(() => { + jest.resetAllMocks() + + mockSocialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) + }) + + it('should render continue with connected account when on gnosis chain', async () => { + const mockOnLogin = jest.fn() + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() + }) + + it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => { + const mockOnLogin = jest.fn() + + const result = render( + + + , + ) + + await waitFor(async () => { + expect(result.findByText('Continue with Google')).resolves.toBeDefined() + expect(await result.findByRole('button')).toBeEnabled() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + act(() => { + button.click() + }) + + await waitFor(async () => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) + + it('should disable the Google Login button with a message when not on gnosis chain', async () => { + const result = render( + , + ) + + expect(result.getByText('Currently only supported on Goerli')).toBeInTheDocument() + expect(await result.findByRole('button')).toBeDisabled() + }) + + it('should display Password Recovery form and call onLogin if password recovery succeeds', async () => { + const mockOnLogin = jest.fn() + mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE)) + mockSocialWalletService.getUserInfo = jest.fn(undefined) + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue with Google')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + + act(() => { + button.click() + }) + + await waitFor(() => { + expect(result.findByText('Enter security password')).resolves.toBeDefined() + }) + + const passwordField = await result.findByLabelText('Recovery password') + const submitButton = await result.findByText('Submit') + + act(() => { + fireEvent.change(passwordField, { target: { value: 'Test1234!' } }) + submitButton.click() + }) + + await waitFor(() => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) + + describe('getSupportedChains', () => { + const mockEthereumChain = chainBuilder() + .with({ + chainId: '1', + chainName: 'Ethereum', + disabledWallets: ['socialLogin'], + }) + .build() + const mockGnosisChain = chainBuilder() + .with({ chainId: '100', chainName: 'Gnosis Chain', disabledWallets: ['Coinbase'] }) + .build() + it('returns chain names where social login is enabled', () => { + const mockGoerliChain = chainBuilder().with({ chainId: '5', chainName: 'Goerli', disabledWallets: [] }).build() + + const mockChains = [mockEthereumChain, mockGnosisChain, mockGoerliChain] + const result = _getSupportedChains(mockChains) + + expect(result).toEqual(['Gnosis Chain', 'Goerli']) + }) + + it('returns an empty array if social login is not enabled on any chain', () => { + const mockGoerliChain = chainBuilder() + .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialLogin'] }) + .build() + + const mockChains = [mockEthereumChain, mockGoerliChain] + const result = _getSupportedChains(mockChains) + + expect(result).toEqual([]) + }) + }) +}) diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/SocialSigner/index.tsx similarity index 71% rename from src/components/common/ConnectWallet/MPCLogin.tsx rename to src/components/common/SocialSigner/index.tsx index 32d50e47bd..a772dba5e0 100644 --- a/src/components/common/ConnectWallet/MPCLogin.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -1,23 +1,23 @@ -import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' import { Box, Button, SvgIcon, Typography } from '@mui/material' -import { useCallback, useContext, useMemo } from 'react' -import { MpcWalletContext } from './MPCWalletProvider' -import { PasswordRecovery } from './PasswordRecovery' +import { useCallback, useContext, useMemo, useState } from 'react' +import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' import InfoIcon from '@/public/images/notifications/info.svg' import css from './styles.module.css' import useWallet from '@/hooks/wallets/useWallet' -import Track from '../Track' +import Track from '@/components/common/Track' import { CREATE_SAFE_EVENTS } from '@/services/analytics' import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import useChains, { useCurrentChain } from '@/hooks/useChains' import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' -import { isSocialLoginWallet } from '@/services/mpc/module' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { CGW_NAMES } from '@/hooks/wallets/consts' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TxModalContext } from '@/components/tx-flow' import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' +import madProps from '@/utils/mad-props' export const _getSupportedChains = (chains: ChainInfo[]) => { return chains @@ -38,47 +38,67 @@ const useIsSocialWalletEnabled = () => { return isSocialWalletEnabled(currentChain) } -const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { - const { triggerLogin, userInfo, walletState, setWalletState, recoverFactorWithPassword } = - useContext(MpcWalletContext) +type SocialSignerLoginProps = { + socialWalletService: ReturnType + wallet: ReturnType + supportedChains: ReturnType + isMPCLoginEnabled: ReturnType + onLogin?: () => void +} + +export const SocialSigner = ({ + socialWalletService, + wallet, + supportedChains, + isMPCLoginEnabled, + onLogin, +}: SocialSignerLoginProps) => { + const [loginPending, setLoginPending] = useState(false) const { setTxFlow } = useContext(TxModalContext) + const userInfo = socialWalletService?.getUserInfo() + const isDisabled = loginPending || !isMPCLoginEnabled - const wallet = useWallet() - const loginPending = walletState === MPCWalletState.AUTHENTICATING + const recoverPassword = useCallback( + async (password: string, storeDeviceFactor: boolean) => { + if (!socialWalletService) return - const supportedChains = useGetSupportedChains() - const isMPCLoginEnabled = useIsSocialWalletEnabled() + const success = await socialWalletService.recoverAccountWithPassword(password, storeDeviceFactor) - const isDisabled = loginPending || !isMPCLoginEnabled + if (success) { + onLogin?.() + setTxFlow(undefined) + } + }, + [onLogin, setTxFlow, socialWalletService], + ) const login = async () => { - const status = await triggerLogin() + if (!socialWalletService) return + + setLoginPending(true) + + const status = await socialWalletService.loginAndCreate() if (status === COREKIT_STATUS.LOGGED_IN) { onLogin?.() + setLoginPending(false) } if (status === COREKIT_STATUS.REQUIRED_SHARE) { setTxFlow( - , - () => setWalletState(MPCWalletState.NOT_INITIALIZED), + { + onLogin?.() + setLoginPending(false) + }} + />, + () => {}, false, ) } } - const recoverPassword = useCallback( - async (password: string, storeDeviceFactor: boolean) => { - const success = await recoverFactorWithPassword(password, storeDeviceFactor) - - if (success) { - onLogin?.() - setTxFlow(undefined) - } - }, - [onLogin, recoverFactorWithPassword, setTxFlow], - ) - const isSocialLogin = isSocialLoginWallet(wallet?.label) return ( @@ -148,4 +168,9 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { ) } -export default MPCLogin +export default madProps(SocialSigner, { + socialWalletService: useSocialWallet, + wallet: useWallet, + supportedChains: useGetSupportedChains, + isMPCLoginEnabled: useIsSocialWalletEnabled, +}) diff --git a/src/components/common/SocialSigner/styles.module.css b/src/components/common/SocialSigner/styles.module.css new file mode 100644 index 0000000000..92c29890de --- /dev/null +++ b/src/components/common/SocialSigner/styles.module.css @@ -0,0 +1,24 @@ +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.passwordWrapper { + padding: var(--space-4) var(--space-4) var(--space-2) var(--space-4); + display: flex; + flex-direction: column; + align-items: baseline; + gap: var(--space-1); +} + +.loginError { + width: 100%; + margin: 0; +} diff --git a/src/components/common/WalletInfo/index.test.tsx b/src/components/common/WalletInfo/index.test.tsx index f94de5e3a3..e402e7dd4a 100644 --- a/src/components/common/WalletInfo/index.test.tsx +++ b/src/components/common/WalletInfo/index.test.tsx @@ -2,11 +2,12 @@ import { render } from '@/tests/test-utils' import { WalletInfo } from '@/components/common/WalletInfo/index' import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' import { type NextRouter } from 'next/router' -import * as mpcModule from '@/services/mpc/module' +import * as mpcModule from '@/services/mpc/SocialLoginModule' import * as constants from '@/config/constants' -import * as mfaHelper from '@/components/settings/SecurityLogin/SocialSignerMFA/helper' import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { act } from '@testing-library/react' +import SocialWalletService from '@/services/mpc/SocialWalletService' +import type { ISocialWalletService } from '@/services/mpc/interfaces' const mockWallet = { address: '0x1234567890123456789012345678901234567890', @@ -26,17 +27,20 @@ const mockOnboard = { setChain: jest.fn(), } as unknown as OnboardAPI +jest.mock('@/services/mpc/SocialWalletService') + describe('WalletInfo', () => { + let socialWalletService: ISocialWalletService beforeEach(() => { jest.resetAllMocks() + socialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) }) it('should display the wallet address', () => { const { getByText } = render( { const { getByText } = render( { const { getByText } = render( { const { getByText } = render( { const { queryByText } = render( { const { queryByText } = render( { it('should display an enable mfa button if mfa is not enabled', () => { jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(mfaHelper, 'isMFAEnabled').mockReturnValue(false) const { getByText } = render( { it('should not display an enable mfa button if mfa is already enabled', () => { jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(mfaHelper, 'isMFAEnabled').mockReturnValue(true) + + // Mock that MFA is enabled + socialWalletService.enableMFA('', '') const { queryByText } = render( + socialWalletService: ReturnType router: ReturnType onboard: ReturnType addressBook: ReturnType @@ -33,8 +28,7 @@ type WalletInfoProps = { export const WalletInfo = ({ wallet, - resetAccount, - mpcCoreKit, + socialWalletService, router, onboard, addressBook, @@ -50,6 +44,8 @@ export const WalletInfo = ({ } } + const resetAccount = () => socialWalletService?.__deleteAccount() + const handleDisconnect = () => { if (!wallet) return @@ -70,7 +66,7 @@ export const WalletInfo = ({ {isSocialLogin ? ( <> - {mpcCoreKit && !isMFAEnabled(mpcCoreKit) && ( + {socialWalletService && !socialWalletService.isMFAEnabled() && ( + + + setIsModalOpen(false)} open={isModalOpen} /> diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts b/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts deleted file mode 100644 index 94b88cb8f0..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/helper.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { logError } from '@/services/exceptions' -import ErrorCodes from '@/services/exceptions/ErrorCodes' -import { asError } from '@/services/exceptions/utils' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' - -export const isMFAEnabled = (mpcCoreKit: Web3AuthMPCCoreKit) => { - if (!mpcCoreKit) { - return false - } - const { shareDescriptions } = mpcCoreKit.getKeyDetails() - return !Object.values(shareDescriptions).some((value) => value[0]?.includes('hashedShare')) -} - -export const enableMFA = async ( - mpcCoreKit: Web3AuthMPCCoreKit, - { - newPassword, - currentPassword, - }: { - newPassword: string - currentPassword: string | undefined - }, -) => { - if (!mpcCoreKit) { - return - } - const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) - try { - // 1. setup device factor with password recovery - await securityQuestions.upsertPassword(newPassword, currentPassword) - const securityQuestionFactor = await securityQuestions.recoverWithPassword(newPassword) - if (!securityQuestionFactor) { - throw Error('Could not recover using the new password recovery') - } - - if (!isMFAEnabled(mpcCoreKit)) { - trackEvent(MPC_WALLET_EVENTS.ENABLE_MFA) - // 2. enable MFA in mpcCoreKit - await mpcCoreKit.enableMFA({}, false) - } - - await mpcCoreKit.commitChanges() - } catch (e) { - const error = asError(e) - logError(ErrorCodes._304, error.message) - throw error - } -} diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx index 5121193345..bca00660bf 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx @@ -1,5 +1,4 @@ import Track from '@/components/common/Track' -import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' import { Typography, Button, @@ -16,7 +15,6 @@ import { import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { useState, useMemo, type ChangeEvent } from 'react' import { FormProvider, useForm } from 'react-hook-form' -import { enableMFA } from '@/components/settings/SecurityLogin/SocialSignerMFA/helper' import CheckIcon from '@/public/images/common/check-filled.svg' import LockWarningIcon from '@/public/images/common/lock-warning.svg' import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' @@ -24,8 +22,8 @@ import css from '@/components/settings/SecurityLogin/SocialSignerMFA/styles.modu import BarChartIcon from '@/public/images/common/bar-chart.svg' import ShieldIcon from '@/public/images/common/shield.svg' import ShieldOffIcon from '@/public/images/common/shield-off.svg' -import useMPC from '@/hooks/wallets/mpc/useMPC' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' enum PasswordFieldNames { currentPassword = 'currentPassword', @@ -80,7 +78,7 @@ const passwordStrengthMap = { } as const const SocialSignerMFA = () => { - const mpcCoreKit = useMPC() + const socialWalletService = useSocialWallet() const [passwordStrength, setPasswordStrength] = useState() const [submitError, setSubmitError] = useState() const [open, setOpen] = useState(false) @@ -97,17 +95,20 @@ const SocialSignerMFA = () => { const { formState, handleSubmit, reset, watch } = formMethods const isPasswordSet = useMemo(() => { - if (!mpcCoreKit) return false - - const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) - return securityQuestions.isEnabled() - }, [mpcCoreKit]) + if (!socialWalletService) { + return false + } + return socialWalletService.isRecoveryPasswordSet() + }, [socialWalletService]) const onSubmit = async (data: PasswordFormData) => { - if (!mpcCoreKit) return + if (!socialWalletService) return try { - await enableMFA(mpcCoreKit, data) + await socialWalletService.enableMFA( + data[PasswordFieldNames.currentPassword], + data[PasswordFieldNames.newPassword], + ) onReset() setOpen(false) } catch (e) { diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index bb493cb90d..0a6e5f8223 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -2,7 +2,7 @@ import { Grid, Paper, Typography } from '@mui/material' import SocialSignerMFA from '@/components/settings/SecurityLogin/SocialSignerMFA' import SocialSignerExport from '@/components/settings/SecurityLogin/SocialSignerExport' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/module' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' const SecurityLogin = () => { const wallet = useWallet() diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 102b0de6ef..a02d570df8 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -1,7 +1,7 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import Track from '@/components/common/Track' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/module' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { CREATE_SAFE_EVENTS } from '@/services/analytics' import { Box, Button, Typography } from '@mui/material' import { EthHashInfo } from '@safe-global/safe-react-components' diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 998d660307..a80237343a 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,4 +1,4 @@ -import MPCLogin from '@/components/common/ConnectWallet/MPCLogin' +import SocialSigner from '@/components/common/SocialSigner' import { AppRoutes } from '@/config/routes' import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' import SafeLogo from '@/public/images/logo-text.svg' @@ -35,7 +35,7 @@ const WelcomeLogin = () => { - + Already have a Safe Account? diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index edf9dfe9c3..a35c4875c0 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -4,7 +4,7 @@ import { _getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../us import * as useChains from '@/hooks/useChains' import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' import { hexZeroPad } from 'ethers/lib/utils' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' import { type Web3AuthMPCCoreKit, COREKIT_STATUS } from '@web3auth/mpc-core-kit' import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' @@ -13,6 +13,8 @@ jest.mock('@web3auth/mpc-core-kit', () => ({ Web3AuthMPCCoreKit: jest.fn(), })) +jest.mock('@/hooks/wallets/mpc/useSocialWallet') + type MPCProvider = Web3AuthMPCCoreKit['provider'] /** diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts deleted file mode 100644 index 176e7b777f..0000000000 --- a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { act, renderHook, waitFor } from '@/tests/test-utils' -import { MPCWalletState, useMPCWallet } from '../useMPCWallet' -import * as useOnboard from '@/hooks/wallets/useOnboard' -import { type OnboardAPI } from '@web3-onboard/core' -import { - COREKIT_STATUS, - type UserInfo, - type OauthLoginParams, - type Web3AuthMPCCoreKit, - type TssSecurityQuestion, -} from '@web3auth/mpc-core-kit' -import * as mpcCoreKit from '@web3auth/mpc-core-kit' -import { setMPCCoreKitInstance } from '../useMPC' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' -import { ethers } from 'ethers' -import BN from 'bn.js' -import * as addressBookSlice from '@/store/addressBookSlice' -import * as useChainId from '@/hooks/useChainId' -import { hexZeroPad } from 'ethers/lib/utils' -import * as useAddressBook from '@/hooks/useAddressBook' - -/** time until mock login resolves */ -const MOCK_LOGIN_TIME = 1000 -/** Mock address for successful login */ -const mockSignerAddress = hexZeroPad('0x1', 20) - -/** - * Helper class for mocking MPC Core Kit login flow - */ -class MockMPCCoreKit { - status: COREKIT_STATUS = COREKIT_STATUS.INITIALIZED - state: { - userInfo: UserInfo | undefined - } = { - userInfo: undefined, - } - - private stateAfterLogin: COREKIT_STATUS - private userInfoAfterLogin: UserInfo | undefined - private expectedFactorKey: BN - /** - * - * @param stateAfterLogin State after loginWithOauth resolves - * @param userInfoAfterLogin User info to set in the state after loginWithOauth resolves - * @param expectedFactorKey For MFA login flow the expected factor key. If inputFactorKey gets called with the expected factor key the state switches to logged in - */ - constructor(stateAfterLogin: COREKIT_STATUS, userInfoAfterLogin: UserInfo, expectedFactorKey: BN = new BN(-1)) { - this.stateAfterLogin = stateAfterLogin - this.userInfoAfterLogin = userInfoAfterLogin - this.expectedFactorKey = expectedFactorKey - } - - loginWithOauth(params: OauthLoginParams): Promise { - return new Promise((resolve) => { - // Resolve after 1 sec - setTimeout(() => { - this.status = this.stateAfterLogin - this.state.userInfo = this.userInfoAfterLogin - resolve() - }, MOCK_LOGIN_TIME) - }) - } - - inputFactorKey(factorKey: BN) { - if (factorKey.eq(this.expectedFactorKey)) { - this.status = COREKIT_STATUS.LOGGED_IN - return Promise.resolve() - } else { - Promise.reject() - } - } - - commitChanges() { - return Promise.resolve() - } - - getUserInfo() { - return this.state.userInfo - } -} - -describe('useMPCWallet', () => { - beforeAll(() => { - jest.useFakeTimers() - }) - beforeEach(() => { - jest.resetAllMocks() - setMPCCoreKitInstance(undefined) - jest.spyOn(useChainId, 'default').mockReturnValue('100') - }) - afterAll(() => { - jest.useRealTimers() - }) - it('should have state NOT_INITIALIZED initially', () => { - const { result } = renderHook(() => useMPCWallet()) - expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) - expect(result.current.userInfo?.email).toBeUndefined() - }) - - describe('triggerLogin', () => { - it('should throw if Onboard is not initialized', () => { - const { result } = renderHook(() => useMPCWallet()) - expect(result.current.triggerLogin()).rejects.toEqual(new Error('Onboard is not initialized')) - expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) - }) - - it('should throw if MPC Core Kit is not initialized', () => { - jest.spyOn(useAddressBook, 'default').mockReturnValue({}) - jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) - const { result } = renderHook(() => useMPCWallet()) - - expect(result.current.triggerLogin()).rejects.toEqual(new Error('MPC Core Kit is not initialized')) - expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) - }) - - it('should handle successful log in for SFA account', async () => { - jest.spyOn(useAddressBook, 'default').mockReturnValue({}) - const upsertABSpy = jest.spyOn(addressBookSlice, 'upsertAddressBookEntry') - jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) - const connectWalletSpy = jest.fn().mockResolvedValue([{ accounts: [{ address: mockSignerAddress }] }]) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) - setMPCCoreKitInstance( - new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit, - ) - const { result } = renderHook(() => useMPCWallet()) - - let status: Promise - act(() => { - status = result.current.triggerLogin() - }) - - // While the login resolves we are in Authenticating state - expect(result.current.walletState === MPCWalletState.AUTHENTICATING) - expect(connectWalletSpy).not.toBeCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // We should be logged in and onboard should get connected - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) - expect(result.current.walletState === MPCWalletState.READY) - expect(connectWalletSpy).toBeCalledWith(expect.anything(), { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }) - expect(upsertABSpy).toBeCalledWith({ address: mockSignerAddress, name: 'test@test.com', chainId: '100' }) - }) - }) - - it('should handle successful log in for MFA account with device share', async () => { - jest.spyOn(useAddressBook, 'default').mockReturnValue({ [mockSignerAddress]: 'Some name' }) - const upsertABSpy = jest.spyOn(addressBookSlice, 'upsertAddressBookEntry') - const mockDeviceFactor = ethers.Wallet.createRandom().privateKey.slice(2) - jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) - const connectWalletSpy = jest.fn().mockResolvedValue([{ accounts: [{ address: mockSignerAddress }] }]) - - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) - setMPCCoreKitInstance( - new MockMPCCoreKit( - COREKIT_STATUS.REQUIRED_SHARE, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(mockDeviceFactor, 'hex'), - ) as unknown as Web3AuthMPCCoreKit, - ) - - jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(mockDeviceFactor)) - - const { result } = renderHook(() => useMPCWallet()) - - let status: Promise - act(() => { - status = result.current.triggerLogin() - }) - - // While the login resolves we are in Authenticating state - expect(result.current.walletState === MPCWalletState.AUTHENTICATING) - expect(connectWalletSpy).not.toBeCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // We should be logged in and onboard should get connected - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) - expect(result.current.walletState === MPCWalletState.READY) - expect(connectWalletSpy).toBeCalledWith(expect.anything(), { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }) - expect(upsertABSpy).not.toHaveBeenCalled() - }) - }) - - it('should require manual share for MFA account without device share', async () => { - jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) - setMPCCoreKitInstance( - new MockMPCCoreKit(COREKIT_STATUS.REQUIRED_SHARE, { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit, - ) - - jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(undefined)) - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - } as unknown as TssSecurityQuestion) - - const { result } = renderHook(() => useMPCWallet()) - - let status: Promise - act(() => { - status = result.current.triggerLogin() - }) - - // While the login resolves we are in Authenticating state - expect(result.current.walletState === MPCWalletState.AUTHENTICATING) - expect(connectWalletSpy).not.toBeCalled() - - // Resolve mock login - act(() => { - jest.advanceTimersByTime(MOCK_LOGIN_TIME) - }) - - // A missing second factor should result in manual recovery state - await waitFor(() => { - expect(status).resolves.toEqual(COREKIT_STATUS.REQUIRED_SHARE) - expect(result.current.walletState === MPCWalletState.MANUAL_RECOVERY) - expect(connectWalletSpy).not.toBeCalled() - }) - }) - }) - - describe('resetAccount', () => { - it('should throw if mpcCoreKit is not initialized', () => { - const { result } = renderHook(() => useMPCWallet()) - expect(result.current.resetAccount()).rejects.toEqual( - new Error('MPC Core Kit is not initialized or the user is not logged in'), - ) - }) - it('should reset an account by overwriting the metadata', async () => { - const mockSetMetadata = jest.fn() - const mockMPCCore = { - metadataKey: ethers.Wallet.createRandom().privateKey.slice(2), - state: { - userInfo: undefined, - }, - tKey: { - storageLayer: { - setMetadata: mockSetMetadata, - }, - }, - } - - setMPCCoreKitInstance(mockMPCCore as unknown as Web3AuthMPCCoreKit) - - const { result } = renderHook(() => useMPCWallet()) - - await result.current.resetAccount() - - expect(mockSetMetadata).toHaveBeenCalledWith({ - privKey: new BN(mockMPCCore.metadataKey, 'hex'), - input: { message: 'KEY_NOT_FOUND' }, - }) - }) - }) - - describe('recoverFactorWithPassword', () => { - it('should throw if mpcCoreKit is not initialized', () => { - const { result } = renderHook(() => useMPCWallet()) - expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual( - new Error('MPC Core Kit is not initialized'), - ) - }) - - it('should not recover if wrong password is entered', () => { - setMPCCoreKitInstance({ - state: { - userInfo: undefined, - }, - } as unknown as Web3AuthMPCCoreKit) - const { result } = renderHook(() => useMPCWallet()) - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - recoverFactor: () => { - throw new Error('Invalid answer') - }, - } as unknown as TssSecurityQuestion) - - expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual(new Error('Invalid answer')) - }) - - it('should input recovered factor if correct password is entered', async () => { - const mockSecurityQuestionFactor = ethers.Wallet.createRandom().privateKey.slice(2) - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) - - setMPCCoreKitInstance( - new MockMPCCoreKit( - COREKIT_STATUS.REQUIRED_SHARE, - { - email: 'test@test.com', - name: 'Test', - } as unknown as UserInfo, - new BN(mockSecurityQuestionFactor, 'hex'), - ) as unknown as Web3AuthMPCCoreKit, - ) - - const { result } = renderHook(() => useMPCWallet()) - jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ - getQuestion: () => 'SOME RANDOM QUESTION', - recoverFactor: () => Promise.resolve(mockSecurityQuestionFactor), - } as unknown as TssSecurityQuestion) - - act(() => result.current.recoverFactorWithPassword('test', false)) - - await waitFor(() => { - expect(result.current.walletState === MPCWalletState.READY) - expect(connectWalletSpy).toBeCalledWith(expect.anything(), { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }) - }) - }) - }) -}) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index 0e364d2454..611dede8d5 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -7,13 +7,15 @@ import { WEB3_AUTH_CLIENT_ID } from '@/config/constants' import { useCurrentChain } from '@/hooks/useChains' import { getRpcServiceUrl } from '../web3' import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { useInitSocialWallet } from './useSocialWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' const { getStore, setStore, useStore } = new ExternalStore() export const useInitMPC = () => { const chain = useCurrentChain() const onboard = useOnboard() + useInitSocialWallet() useEffect(() => { if (!chain || !onboard) { diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts deleted file mode 100644 index 1c7c1416af..0000000000 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { type Dispatch, type SetStateAction, useState } from 'react' -import useMPC from './useMPC' -import BN from 'bn.js' -import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' -import { COREKIT_STATUS, getWebBrowserFactor, type UserInfo } from '@web3auth/mpc-core-kit' -import useOnboard, { connectWallet } from '../useOnboard' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' -import { SecurityQuestionRecovery } from './recovery/SecurityQuestionRecovery' -import { DeviceShareRecovery } from './recovery/DeviceShareRecovery' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import useAddressBook from '@/hooks/useAddressBook' -import { upsertAddressBookEntry } from '@/store/addressBookSlice' -import { useAppDispatch } from '@/store' -import useChainId from '@/hooks/useChainId' -import { checksumAddress } from '@/utils/addresses' - -export enum MPCWalletState { - NOT_INITIALIZED, - AUTHENTICATING, - MANUAL_RECOVERY, - READY, -} - -export type MPCWalletHook = { - upsertPasswordBackup: (password: string) => Promise - recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise - walletState: MPCWalletState - setWalletState: Dispatch> - triggerLogin: () => Promise - resetAccount: () => Promise - userInfo: UserInfo | undefined - exportPk: (password: string) => Promise -} - -export const useMPCWallet = (): MPCWalletHook => { - const [walletState, setWalletState] = useState(MPCWalletState.NOT_INITIALIZED) - const mpcCoreKit = useMPC() - const onboard = useOnboard() - const addressBook = useAddressBook() - const currentChainId = useChainId() - const dispatch = useAppDispatch() - - const criticalResetAccount = async (): Promise => { - // This is a critical function that should only be used for testing purposes - // Resetting your account means clearing all the metadata associated with it from the metadata server - // The key details will be deleted from our server and you will not be able to recover your account - if (!mpcCoreKit?.metadataKey) { - throw new Error('MPC Core Kit is not initialized or the user is not logged in') - } - - // In web3auth an account is reset by overwriting the metadata with KEY_NOT_FOUND - await mpcCoreKit.tKey.storageLayer.setMetadata({ - privKey: new BN(mpcCoreKit.metadataKey, 'hex'), - input: { message: 'KEY_NOT_FOUND' }, - }) - } - - const triggerLogin = async () => { - if (!onboard) { - throw Error('Onboard is not initialized') - } - - if (!mpcCoreKit) { - throw Error('MPC Core Kit is not initialized') - } - try { - setWalletState(MPCWalletState.AUTHENTICATING) - await mpcCoreKit.loginWithOauth({ - subVerifierDetails: { - typeOfLogin: 'google', - verifier: WEB3AUTH_VERIFIER_ID, - clientId: GOOGLE_CLIENT_ID, - }, - }) - - if (mpcCoreKit.status === COREKIT_STATUS.REQUIRED_SHARE) { - // Check if we have a device share stored - const deviceFactor = await getWebBrowserFactor(mpcCoreKit) - if (deviceFactor) { - // Recover from device factor - const deviceFactorKey = new BN(deviceFactor, 'hex') - await mpcCoreKit.inputFactorKey(deviceFactorKey) - } else { - // Check password recovery - const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) - if (securityQuestions.isEnabled()) { - trackEvent(MPC_WALLET_EVENTS.MANUAL_RECOVERY) - setWalletState(MPCWalletState.MANUAL_RECOVERY) - return mpcCoreKit.status - } - } - } - - await finalizeLogin() - return mpcCoreKit.status - } catch (error) { - setWalletState(MPCWalletState.NOT_INITIALIZED) - console.error(error) - return mpcCoreKit.status - } - } - - const finalizeLogin = async () => { - if (!mpcCoreKit || !onboard) { - return - } - - if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - await mpcCoreKit.commitChanges() - - const wallets = await connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - - // If the signer is not in the address book => add the user's email as name - if (wallets && currentChainId && wallets.length > 0) { - const address = wallets[0].accounts[0]?.address - if (address) { - const signerAddress = checksumAddress(address) - if (addressBook[signerAddress] === undefined) { - const email = mpcCoreKit.getUserInfo().email - dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) - } - } - } - setWalletState(MPCWalletState.READY) - } - } - - const recoverFactorWithPassword = async (password: string, storeDeviceShare: boolean = false) => { - if (!mpcCoreKit) { - throw new Error('MPC Core Kit is not initialized') - } - - const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) - - if (securityQuestions.isEnabled()) { - const factorKeyString = await securityQuestions.recoverWithPassword(password) - const factorKey = new BN(factorKeyString, 'hex') - await mpcCoreKit.inputFactorKey(factorKey) - - if (storeDeviceShare) { - const deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) - await deviceShareRecovery.createAndStoreDeviceFactor() - } - - await finalizeLogin() - } - - return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN - } - - const exportPk = async (password: string): Promise => { - if (!mpcCoreKit) { - throw new Error('MPC Core Kit is not initialized') - } - const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) - - try { - if (securityQuestions.isEnabled()) { - // Only export PK if recovery works - await securityQuestions.recoverWithPassword(password) - } - const exportedPK = await mpcCoreKit?._UNSAFE_exportTssKey() - return exportedPK - } catch (err) { - throw new Error('Error exporting account. Make sure the password is correct.') - } - } - - return { - triggerLogin, - walletState, - setWalletState, - recoverFactorWithPassword, - resetAccount: criticalResetAccount, - upsertPasswordBackup: () => Promise.resolve(), - userInfo: mpcCoreKit?.state.userInfo, - exportPk, - } -} diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts new file mode 100644 index 0000000000..d98998ffdd --- /dev/null +++ b/src/hooks/wallets/mpc/useSocialWallet.ts @@ -0,0 +1,63 @@ +import useAddressBook from '@/hooks/useAddressBook' +import useChainId from '@/hooks/useChainId' +import ExternalStore from '@/services/ExternalStore' +import type { ISocialWalletService } from '@/services/mpc/interfaces' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import SocialWalletService from '@/services/mpc/SocialWalletService' +import { useAppDispatch } from '@/store' +import { upsertAddressBookEntry } from '@/store/addressBookSlice' +import { checksumAddress } from '@/utils/addresses' +import { useCallback, useEffect } from 'react' +import useOnboard, { connectWallet } from '../useOnboard' +import useMpc from './useMPC' + +const { getStore, setStore, useStore } = new ExternalStore() + +export const useInitSocialWallet = () => { + const mpcCoreKit = useMpc() + const onboard = useOnboard() + const addressBook = useAddressBook() + const currentChainId = useChainId() + const dispatch = useAppDispatch() + const socialWalletService = useStore() + + const onConnect = useCallback(async () => { + if (!onboard || !socialWalletService) return + + const wallets = await connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + + // If the signer is not in the address book => add the user's email as name + const userInfo = socialWalletService.getUserInfo() + if (userInfo && wallets && currentChainId && wallets.length > 0) { + const address = wallets[0].accounts[0]?.address + if (address) { + const signerAddress = checksumAddress(address) + if (addressBook[signerAddress] === undefined) { + const email = userInfo.email + dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) + } + } + } + }, [addressBook, currentChainId, dispatch, onboard, socialWalletService]) + + useEffect(() => { + socialWalletService?.setOnConnect(onConnect) + }, [onConnect, socialWalletService]) + + useEffect(() => { + if (mpcCoreKit) { + setStore(new SocialWalletService(mpcCoreKit)) + } + }, [mpcCoreKit]) +} + +export const getSocialWalletService = getStore + +export const __setSocialWalletService = setStore + +export default useStore diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index 50b3aeeb44..c55c23f9c2 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -12,7 +12,7 @@ import walletConnect from '@web3-onboard/walletconnect' import pairingModule from '@/services/pairing/module' import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' -import MpcModule from '@/services/mpc/module' +import MpcModule from '@/services/mpc/SocialLoginModule' const prefersDarkMode = (): boolean => { return window?.matchMedia('(prefers-color-scheme: dark)')?.matches diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 36bdd4d979..5133a92961 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -38,7 +38,6 @@ import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendin import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' import { useInitMPC } from '@/hooks/wallets/mpc/useMPC' -import { MpcWalletProvider } from '@/components/common/ConnectWallet/MPCWalletProvider' import useABTesting from '@/services/tracking/useAbTesting' import { AbTest } from '@/services/tracking/abTesting' import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' @@ -81,9 +80,7 @@ export const AppProviders = ({ children }: { children: ReactNode | ReactNode[] } {(safeTheme: Theme) => ( - - {children} - + {children} )} diff --git a/src/services/mpc/module.ts b/src/services/mpc/SocialLoginModule.ts similarity index 100% rename from src/services/mpc/module.ts rename to src/services/mpc/SocialLoginModule.ts diff --git a/src/services/mpc/SocialWalletService.ts b/src/services/mpc/SocialWalletService.ts new file mode 100644 index 0000000000..3fa00d7a7e --- /dev/null +++ b/src/services/mpc/SocialWalletService.ts @@ -0,0 +1,153 @@ +import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import BN from 'bn.js' +import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' +import { SecurityQuestionRecovery } from '@/services/mpc/recovery/SecurityQuestionRecovery' +import { trackEvent } from '@/services/analytics' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' +import { DeviceShareRecovery } from '@/services/mpc/recovery/DeviceShareRecovery' +import { logError } from '../exceptions' +import ErrorCodes from '../exceptions/ErrorCodes' +import { asError } from '../exceptions/utils' +import { type ISocialWalletService } from './interfaces' + +/** + * Singleton Service for accessing the social login wallet + */ +class SocialWalletService implements ISocialWalletService { + private mpcCoreKit: Web3AuthMPCCoreKit + private onConnect: () => Promise = () => Promise.resolve() + + private deviceShareRecovery: DeviceShareRecovery + private securityQuestionRecovery: SecurityQuestionRecovery + + constructor(mpcCoreKit: Web3AuthMPCCoreKit) { + this.mpcCoreKit = mpcCoreKit + this.deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) + this.securityQuestionRecovery = new SecurityQuestionRecovery(mpcCoreKit) + } + + isMFAEnabled() { + const { shareDescriptions } = this.mpcCoreKit.getKeyDetails() + return !Object.values(shareDescriptions).some((value) => value[0]?.includes('hashedShare')) + } + + isRecoveryPasswordSet() { + return this.securityQuestionRecovery.isEnabled() + } + + async enableMFA(oldPassword: string | undefined, newPassword: string): Promise { + try { + // 1. setup device factor with password recovery + await this.securityQuestionRecovery.upsertPassword(newPassword, oldPassword) + const securityQuestionFactor = await this.securityQuestionRecovery.recoverWithPassword(newPassword) + if (!securityQuestionFactor) { + throw Error('Problem setting up the new password') + } + + if (!this.isMFAEnabled()) { + trackEvent(MPC_WALLET_EVENTS.ENABLE_MFA) + // 2. enable MFA in mpcCoreKit + await this.mpcCoreKit.enableMFA({}, false) + } + + await this.mpcCoreKit.commitChanges() + } catch (e) { + const error = asError(e) + logError(ErrorCodes._304, error.message) + throw error + } + } + + setOnConnect(onConnect: () => Promise) { + this.onConnect = onConnect + } + + getUserInfo() { + return this.mpcCoreKit.state.userInfo + } + + async loginAndCreate(): Promise { + try { + await this.mpcCoreKit.loginWithOauth({ + subVerifierDetails: { + typeOfLogin: 'google', + verifier: WEB3AUTH_VERIFIER_ID, + clientId: GOOGLE_CLIENT_ID, + }, + }) + + if (this.mpcCoreKit.status === COREKIT_STATUS.REQUIRED_SHARE) { + // Check if we have a device share stored + if (await this.deviceShareRecovery.isEnabled()) { + await this.deviceShareRecovery.recoverWithDeviceFactor() + } else { + // Check password recovery + if (this.securityQuestionRecovery.isEnabled()) { + trackEvent(MPC_WALLET_EVENTS.MANUAL_RECOVERY) + return this.mpcCoreKit.status + } + } + } + + await this.finalizeLogin() + return this.mpcCoreKit.status + } catch (error) { + console.error(error) + return this.mpcCoreKit.status + } + } + + private async finalizeLogin() { + if (this.mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { + await this.mpcCoreKit.commitChanges() + await this.mpcCoreKit.provider?.request({ method: 'eth_accounts', params: [] }) + await this.onConnect() + } + } + + async recoverAccountWithPassword(password: string, storeDeviceShare: boolean = false) { + if (this.securityQuestionRecovery.isEnabled()) { + const factorKeyString = await this.securityQuestionRecovery.recoverWithPassword(password) + const factorKey = new BN(factorKeyString, 'hex') + await this.mpcCoreKit.inputFactorKey(factorKey) + + if (storeDeviceShare) { + await this.deviceShareRecovery.createAndStoreDeviceFactor() + } + + await this.finalizeLogin() + } + + return this.mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN + } + + async exportSignerKey(password: string): Promise { + try { + if (this.securityQuestionRecovery.isEnabled()) { + // Only export PK if recovery works + await this.securityQuestionRecovery.recoverWithPassword(password) + } + const exportedPK = await this.mpcCoreKit?._UNSAFE_exportTssKey() + return exportedPK + } catch (err) { + throw new Error('Error exporting account. Make sure the password is correct.') + } + } + + async __deleteAccount() { + // This is a critical function that should only be used for testing purposes + // Resetting your account means clearing all the metadata associated with it from the metadata server + // The key details will be deleted from our server and you will not be able to recover your account + if (!this.mpcCoreKit?.metadataKey) { + throw new Error('MPC Core Kit is not initialized or the user is not logged in') + } + + // In web3auth an account is reset by overwriting the metadata with KEY_NOT_FOUND + await this.mpcCoreKit.tKey.storageLayer.setMetadata({ + privKey: new BN(this.mpcCoreKit.metadataKey, 'hex'), + input: { message: 'KEY_NOT_FOUND' }, + }) + } +} + +export default SocialWalletService diff --git a/src/services/mpc/__mocks__/SocialWalletService.ts b/src/services/mpc/__mocks__/SocialWalletService.ts new file mode 100644 index 0000000000..7e78191dc3 --- /dev/null +++ b/src/services/mpc/__mocks__/SocialWalletService.ts @@ -0,0 +1,74 @@ +import { COREKIT_STATUS, type UserInfo } from '@web3auth/mpc-core-kit' +import { hexZeroPad } from 'ethers/lib/utils' +import { type ISocialWalletService } from '../interfaces' + +/** + * Manual mock for SocialWalletService + * + * By default it will log in the user after a 1 second timer. + * For password recovery it expects the password to be "Test1234!" + */ +class TestSocialWalletService implements ISocialWalletService { + private fakePassword = 'Test1234!' + private postLoginState = COREKIT_STATUS.LOGGED_IN + private _isMfaEnabled = false + private onConnect: () => Promise = () => Promise.resolve() + private userInfo: UserInfo = { + email: 'test@testermann.com', + name: 'Test Testermann', + profileImage: 'test.testermann.local/profile.png', + } as unknown as UserInfo + + setOnConnect(onConnect: () => Promise): void { + this.onConnect = onConnect + } + + getUserInfo(): UserInfo | undefined { + return this.userInfo + } + isMFAEnabled(): boolean { + return this._isMfaEnabled + } + enableMFA(oldPassword: string, newPassword: string): Promise { + this._isMfaEnabled = true + return Promise.resolve() + } + isRecoveryPasswordSet(): boolean { + throw new Error('Method not implemented.') + } + + /** + * Method for tests to set the expected login state after calling loginAndCreate() + */ + __setPostLoginState(state: COREKIT_STATUS) { + this.postLoginState = state + } + + __setUserInfo(userInfo: UserInfo) { + this.userInfo = userInfo + } + + async loginAndCreate(): Promise { + return new Promise((resolve) => { + this.onConnect().then(() => resolve(this.postLoginState)) + }) + } + + __deleteAccount(): void { + throw new Error('Method not implemented.') + } + async recoverAccountWithPassword(password: string, storeDeviceFactor: boolean): Promise { + if (this.fakePassword === password) { + await this.onConnect() + return true + } + + throw Error('Invalid Password') + } + + exportSignerKey(password: string): Promise { + return Promise.resolve(hexZeroPad('0x1', 20)) + } +} + +export default TestSocialWalletService diff --git a/src/services/mpc/__tests__/SocialWalletService.test.ts b/src/services/mpc/__tests__/SocialWalletService.test.ts new file mode 100644 index 0000000000..e3163b1069 --- /dev/null +++ b/src/services/mpc/__tests__/SocialWalletService.test.ts @@ -0,0 +1,269 @@ +import { act, waitFor } from '@/tests/test-utils' +import { + COREKIT_STATUS, + type UserInfo, + type OauthLoginParams, + type Web3AuthMPCCoreKit, + type TssSecurityQuestion, +} from '@web3auth/mpc-core-kit' +import * as mpcCoreKit from '@web3auth/mpc-core-kit' +import { ethers } from 'ethers' +import BN from 'bn.js' +import { hexZeroPad } from 'ethers/lib/utils' +import SocialWalletService from '../SocialWalletService' + +/** time until mock login resolves */ +const MOCK_LOGIN_TIME = 1000 +/** Mock address for successful login */ +const mockSignerAddress = hexZeroPad('0x1', 20) + +/** + * Helper class for mocking MPC Core Kit login flow + */ +class MockMPCCoreKit { + status: COREKIT_STATUS = COREKIT_STATUS.INITIALIZED + state: { + userInfo: UserInfo | undefined + } = { + userInfo: undefined, + } + + private stateAfterLogin: COREKIT_STATUS + private userInfoAfterLogin: UserInfo | undefined + private expectedFactorKey: BN + /** + * + * @param stateAfterLogin State after loginWithOauth resolves + * @param userInfoAfterLogin User info to set in the state after loginWithOauth resolves + * @param expectedFactorKey For MFA login flow the expected factor key. If inputFactorKey gets called with the expected factor key the state switches to logged in + */ + constructor(stateAfterLogin: COREKIT_STATUS, userInfoAfterLogin: UserInfo, expectedFactorKey: BN = new BN(-1)) { + this.stateAfterLogin = stateAfterLogin + this.userInfoAfterLogin = userInfoAfterLogin + this.expectedFactorKey = expectedFactorKey + } + + loginWithOauth(params: OauthLoginParams): Promise { + return new Promise((resolve) => { + // Resolve after 1 sec + setTimeout(() => { + this.status = this.stateAfterLogin + this.state.userInfo = this.userInfoAfterLogin + resolve() + }, MOCK_LOGIN_TIME) + }) + } + + inputFactorKey(factorKey: BN) { + if (factorKey.eq(this.expectedFactorKey)) { + this.status = COREKIT_STATUS.LOGGED_IN + return Promise.resolve() + } else { + Promise.reject() + } + } + + commitChanges = jest.fn().mockImplementation(() => Promise.resolve()) + + getUserInfo() { + return this.state.userInfo + } +} + +describe('useMPCWallet', () => { + beforeAll(() => { + jest.useFakeTimers() + }) + beforeEach(() => { + jest.resetAllMocks() + }) + afterAll(() => { + jest.useRealTimers() + }) + + describe('triggerLogin', () => { + it('should handle successful log in for SFA account', async () => { + const mockOnConnect = jest.fn() + + const mockCoreKit = new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit + + const testService = new SocialWalletService(mockCoreKit) + testService.setOnConnect(mockOnConnect) + + let status: Promise + act(() => { + status = testService.loginAndCreate() + }) + + expect(mockOnConnect).not.toHaveBeenCalled() + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // We should be logged in and onboard should get connected + await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) + expect(mockOnConnect).toHaveBeenCalled() + expect(mockCoreKit.commitChanges).toHaveBeenCalled() + }) + }) + + it('should handle successful log in for MFA account with device share', async () => { + const mockOnConnect = jest.fn() + + const mockDeviceFactor = ethers.Wallet.createRandom().privateKey.slice(2) + + const mockCoreKit = new MockMPCCoreKit( + COREKIT_STATUS.REQUIRED_SHARE, + { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo, + new BN(mockDeviceFactor, 'hex'), + ) as unknown as Web3AuthMPCCoreKit + + jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(mockDeviceFactor)) + + const testService = new SocialWalletService(mockCoreKit) + testService.setOnConnect(mockOnConnect) + + let status: Promise + act(() => { + status = testService.loginAndCreate() + }) + + // While the login resolves we are in Authenticating state + expect(mockOnConnect).not.toHaveBeenCalled() + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // We should be logged in and onboard should get connected + await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) + expect(mockOnConnect).toHaveBeenCalled() + expect(mockCoreKit.commitChanges).toHaveBeenCalled() + }) + }) + + it('should require manual share for MFA account without device share', async () => { + const mockOnConnect = jest.fn() + const mockCoreKit = new MockMPCCoreKit(COREKIT_STATUS.REQUIRED_SHARE, { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit + + jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(undefined)) + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + } as unknown as TssSecurityQuestion) + + const testService = new SocialWalletService(mockCoreKit) + testService.setOnConnect(mockOnConnect) + + let status: Promise + act(() => { + status = testService.loginAndCreate() + }) + + // Resolve mock login + act(() => { + jest.advanceTimersByTime(MOCK_LOGIN_TIME) + }) + + // A missing second factor should result in manual recovery state + await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.REQUIRED_SHARE) + expect(mockOnConnect).not.toHaveBeenCalled() + expect(mockCoreKit.commitChanges).not.toHaveBeenCalled() + }) + }) + }) + + describe('resetAccount', () => { + it('should reset an account by overwriting the metadata', async () => { + const mockSetMetadata = jest.fn() + const mockMPCCore = { + metadataKey: ethers.Wallet.createRandom().privateKey.slice(2), + state: { + userInfo: undefined, + }, + tKey: { + storageLayer: { + setMetadata: mockSetMetadata, + }, + }, + } + + const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) + + await testService.__deleteAccount() + + expect(mockSetMetadata).toHaveBeenCalledWith({ + privKey: new BN(mockMPCCore.metadataKey, 'hex'), + input: { message: 'KEY_NOT_FOUND' }, + }) + }) + }) + + describe('recoverFactorWithPassword', () => { + it('should not recover if wrong password is entered', () => { + const mockOnConnect = jest.fn() + const mockMPCCore = { + state: { + userInfo: undefined, + }, + commitChanges: jest.fn(), + } as unknown as Web3AuthMPCCoreKit + + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + recoverFactor: () => { + throw new Error('Invalid answer') + }, + } as unknown as TssSecurityQuestion) + + const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) + testService.setOnConnect(mockOnConnect) + + expect(testService.recoverAccountWithPassword('test', false)).rejects.toEqual(new Error('Invalid answer')) + expect(mockOnConnect).not.toHaveBeenCalled() + expect(mockMPCCore.commitChanges).not.toHaveBeenCalled() + }) + + it('should input recovered factor if correct password is entered', async () => { + const mockOnConnect = jest.fn() + const mockSecurityQuestionFactor = ethers.Wallet.createRandom().privateKey.slice(2) + + const mockMPCCore = new MockMPCCoreKit( + COREKIT_STATUS.REQUIRED_SHARE, + { + email: 'test@test.com', + name: 'Test', + } as unknown as UserInfo, + new BN(mockSecurityQuestionFactor, 'hex'), + ) as unknown as Web3AuthMPCCoreKit + + jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({ + getQuestion: () => 'SOME RANDOM QUESTION', + recoverFactor: () => Promise.resolve(mockSecurityQuestionFactor), + } as unknown as TssSecurityQuestion) + + const testService = new SocialWalletService(mockMPCCore as unknown as Web3AuthMPCCoreKit) + testService.setOnConnect(mockOnConnect) + + act(() => testService.recoverAccountWithPassword('test', false)) + + await waitFor(() => { + expect(mockOnConnect).toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/services/mpc/__tests__/module.test.ts b/src/services/mpc/__tests__/module.test.ts index 3a03f09c81..350bdf65bb 100644 --- a/src/services/mpc/__tests__/module.test.ts +++ b/src/services/mpc/__tests__/module.test.ts @@ -1,4 +1,4 @@ -import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../module' +import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../SocialLoginModule' import { type WalletModule } from '@web3-onboard/common' import * as web3 from '@/hooks/wallets/web3' diff --git a/src/services/mpc/interfaces.ts b/src/services/mpc/interfaces.ts new file mode 100644 index 0000000000..b76ace0eec --- /dev/null +++ b/src/services/mpc/interfaces.ts @@ -0,0 +1,52 @@ +import type { COREKIT_STATUS, UserInfo } from '@web3auth/mpc-core-kit' + +export interface ISocialWalletService { + /** + * Opens a popup with the Google login and creates / restores the mpc wallet. + * + * @returns the follow up status of the mpcCoreKit. + */ + loginAndCreate(): Promise + + /** + * Deletes the currently logged in account. + * This should only be used in dev environments and never in prod! + */ + __deleteAccount(): void + + /** + * Tries to recover a social signer through the Security Questions module + * + * @param password entered recovery password + * @param storeDeviceFactor if true a device factor will be added after successful recovery + */ + recoverAccountWithPassword(password: string, storeDeviceFactor: boolean): Promise + + /** + * Exports the key of the signer + * + * @param password recovery password + */ + exportSignerKey(password: string): Promise + + /** + * Returns true if MFA is enabled + */ + isMFAEnabled(): boolean + + /** + * Enables MFA and stores a device share with 2 factors: + * - one factor encrypted with the password + * - one factor encrypted with a key in the local storage of the browser + * + * @param oldPassword required if MFA is already enabled + * @param newPassword new password to set + */ + enableMFA(oldPassword: string | undefined, newPassword: string): Promise + + isRecoveryPasswordSet(): boolean + + getUserInfo(): UserInfo | undefined + + setOnConnect(onConnect: () => Promise): void +} diff --git a/src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts b/src/services/mpc/recovery/DeviceShareRecovery.ts similarity index 79% rename from src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts rename to src/services/mpc/recovery/DeviceShareRecovery.ts index e5a5fc4054..7806f91b92 100644 --- a/src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts +++ b/src/services/mpc/recovery/DeviceShareRecovery.ts @@ -16,9 +16,6 @@ export class DeviceShareRecovery { } async isEnabled() { - if (!this.mpcCoreKit.tKey.metadata) { - return false - } return !!(await getWebBrowserFactor(this.mpcCoreKit)) } @@ -32,6 +29,16 @@ export class DeviceShareRecovery { await storeWebBrowserFactor(deviceFactorKey, this.mpcCoreKit) } + async recoverWithDeviceFactor() { + // Recover from device factor + const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit) + if (!deviceFactor) { + throw Error('Cannot recover from device factor. No device factor found') + } + const deviceFactorKey = new BN(deviceFactor, 'hex') + await this.mpcCoreKit.inputFactorKey(deviceFactorKey) + } + async removeDeviceFactor() { const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit) if (!deviceFactor) { diff --git a/src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts b/src/services/mpc/recovery/SecurityQuestionRecovery.ts similarity index 100% rename from src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts rename to src/services/mpc/recovery/SecurityQuestionRecovery.ts diff --git a/src/tests/builders/eip1193Provider.ts b/src/tests/builders/eip1193Provider.ts new file mode 100644 index 0000000000..5896fe9b5f --- /dev/null +++ b/src/tests/builders/eip1193Provider.ts @@ -0,0 +1,10 @@ +import { type EIP1193Provider } from '@web3-onboard/core' +import { Builder } from '../Builder' + +export const eip1193ProviderBuilder = () => + Builder.new().with({ + disconnect: jest.fn(), + on: jest.fn(), + removeListener: jest.fn(), + request: jest.fn(), + }) diff --git a/src/tests/builders/wallet.ts b/src/tests/builders/wallet.ts new file mode 100644 index 0000000000..ce2d601b8d --- /dev/null +++ b/src/tests/builders/wallet.ts @@ -0,0 +1,16 @@ +import { type ConnectedWallet } from '@/services/onboard' +import { faker } from '@faker-js/faker' +import { Builder, type IBuilder } from '../Builder' +import { eip1193ProviderBuilder } from './eip1193Provider' + +const walletNames = ['MetaMask', 'Wallet Connect', 'Social Login', 'Rainbow'] + +export const connectedWalletBuilder = (): IBuilder => { + return Builder.new().with({ + address: faker.finance.ethereumAddress(), + chainId: faker.string.numeric(), + ens: faker.string.alpha() + '.ens', + label: faker.helpers.arrayElement(walletNames), + provider: eip1193ProviderBuilder().build(), + }) +} From 351b3607a89d58dad8e7928fa06a4b4ae84c9136 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:07:31 +0100 Subject: [PATCH 27/52] fix: Catch errors during signing with social login (#2713) --- src/components/common/SocialSigner/index.tsx | 5 +++++ src/services/mpc/SocialLoginModule.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index a772dba5e0..f230e3ec57 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -82,6 +82,7 @@ export const SocialSigner = ({ if (status === COREKIT_STATUS.LOGGED_IN) { onLogin?.() setLoginPending(false) + return } if (status === COREKIT_STATUS.REQUIRED_SHARE) { @@ -96,7 +97,11 @@ export const SocialSigner = ({ () => {}, false, ) + return } + + // TODO: Show error if login fails + setLoginPending(false) } const isSocialLogin = isSocialLoginWallet(wallet?.label) diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts index 0a5e87045e..f9921000b3 100644 --- a/src/services/mpc/SocialLoginModule.ts +++ b/src/services/mpc/SocialLoginModule.ts @@ -45,7 +45,10 @@ function MpcModule(): WalletInit { * We have to fallback to web3ReadOnly for eth_estimateGas because the provider by Web3Auth does not expose / implement it. */ if ('eth_estimateGas' === request.method) { - web3ReadOnly?.send(request.method, request.params ?? []).then(resolve) + web3ReadOnly + ?.send(request.method, request.params ?? []) + .then(resolve) + .catch(reject) return } @@ -53,7 +56,7 @@ function MpcModule(): WalletInit { * If the provider is defined we already have access to the accounts. So we can just reply with the current account. */ if ('eth_requestAccounts' === request.method) { - web3.request({ method: 'eth_accounts' }).then(resolve) + web3.request({ method: 'eth_accounts' }).then(resolve).catch(reject) return } @@ -64,7 +67,7 @@ function MpcModule(): WalletInit { } // Default: we call the inner provider - web3.request(request).then(resolve) + web3.request(request).then(resolve).catch(reject) return } catch (error) { reject( From 4d2a81a57bd9013ea937d9a070e05e7e93174d30 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:24:34 +0100 Subject: [PATCH 28/52] [Seedless Onboarding] Only continue to safe creation if login succeeds (#2714) * fix: Only continue to safe creation if login succeeds * fix: Add new test case --- .../welcome/WelcomeLogin/WalletLogin.tsx | 3 ++- .../__tests__/WalletLogin.test.tsx | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index a02d570df8..54242c585f 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -12,7 +12,8 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const login = async () => { const walletState = await connectWallet() - if (walletState) { + + if (walletState && walletState.length > 0) { onLogin() } } diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx index a846dd5f8d..2f28d230f1 100644 --- a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -68,4 +68,27 @@ describe('WalletLogin', () => { expect(mockOnLogin).toHaveBeenCalled() }) }) + + it('should not invoke the callback if connection fails', async () => { + const mockOnLogin = jest.fn() + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn().mockReturnValue([])) + + const result = render() + + await waitFor(() => { + expect(result.findByText('Connect wallet')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + act(() => { + const button = result.getByRole('button') + button.click() + }) + + await waitFor(() => { + expect(mockOnLogin).not.toHaveBeenCalled() + }) + }) }) From 95ba442b313f1c9b5c3fc639be4365f60f0c5b15 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:45:41 +0100 Subject: [PATCH 29/52] [Seedless Onboarding] Add alternative welcome page (#2704) * feat: Add alternative welcome page * fix: Add dark mode support * refactor: Change route to /welcome/social-login * chore: Adjust job permissions --- .github/workflows/lint.yml | 1 + .github/workflows/test.yml | 1 + src/components/common/ErrorBoundary/index.tsx | 2 +- src/components/common/Footer/index.tsx | 2 +- src/components/common/Header/index.tsx | 2 +- .../common/NetworkSelector/index.tsx | 2 +- .../common/SafeLoadingError/index.tsx | 2 +- .../__tests__/useSyncSafeCreationStep.test.ts | 2 +- src/components/new-safe/create/index.tsx | 2 +- .../create/steps/SetNameStep/index.tsx | 2 +- .../create/steps/StatusStep/index.tsx | 2 +- .../create/useSyncSafeCreationStep.ts | 2 +- src/components/new-safe/load/index.tsx | 2 +- src/components/sidebar/SafeList/index.tsx | 8 +- src/components/sidebar/Sidebar/index.tsx | 2 +- src/components/welcome/NewSafe.tsx | 50 +------ src/components/welcome/NewSafeSocial.tsx | 87 +++++++++++ .../{ => SafeListDrawer}/DataWidget.tsx | 0 .../welcome/SafeListDrawer/index.tsx | 55 +++++++ .../welcome/SafeListDrawer/styles.module.css | 33 +++++ src/components/welcome/styles.module.css | 135 +++++++++++------- src/config/routes.ts | 5 +- src/pages/index.tsx | 4 +- src/pages/{welcome.tsx => welcome/index.tsx} | 0 src/pages/welcome/social-login.tsx | 17 +++ 25 files changed, 300 insertions(+), 120 deletions(-) create mode 100644 src/components/welcome/NewSafeSocial.tsx rename src/components/welcome/{ => SafeListDrawer}/DataWidget.tsx (100%) create mode 100644 src/components/welcome/SafeListDrawer/index.tsx create mode 100644 src/components/welcome/SafeListDrawer/styles.module.css rename src/pages/{welcome.tsx => welcome/index.tsx} (100%) create mode 100644 src/pages/welcome/social-login.tsx diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d9bf694363..bc77009b65 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,7 @@ concurrency: jobs: eslint: + permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a25c47ab2a..3a1dd52e3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,7 @@ concurrency: jobs: test: + permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/src/components/common/ErrorBoundary/index.tsx b/src/components/common/ErrorBoundary/index.tsx index e277db308a..f25c1bbcfa 100644 --- a/src/components/common/ErrorBoundary/index.tsx +++ b/src/components/common/ErrorBoundary/index.tsx @@ -33,7 +33,7 @@ const ErrorBoundary: FallbackRender = ({ error, componentStack }) => { {componentStack} )} - + Go home diff --git a/src/components/common/Footer/index.tsx b/src/components/common/Footer/index.tsx index afd19922e0..545b1f43aa 100644 --- a/src/components/common/Footer/index.tsx +++ b/src/components/common/Footer/index.tsx @@ -11,7 +11,7 @@ import MUILink from '@mui/material/Link' import { IS_DEV, IS_OFFICIAL_HOST } from '@/config/constants' const footerPages = [ - AppRoutes.welcome, + AppRoutes.welcome.index, AppRoutes.settings.index, AppRoutes.imprint, AppRoutes.privacy, diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index c18ebcd039..6919ca1526 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -34,7 +34,7 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { const enableWc = !!chain && hasFeature(chain, FEATURES.NATIVE_WALLETCONNECT) // Logo link: if on Dashboard, link to Welcome, otherwise to the root (which redirects to either Dashboard or Welcome) - const logoHref = router.pathname === AppRoutes.home ? AppRoutes.welcome : AppRoutes.index + const logoHref = router.pathname === AppRoutes.home ? AppRoutes.welcome.index : AppRoutes.index const handleMenuToggle = () => { if (onMenuToggle) { diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index dc079f7fea..5e3ac1e676 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -15,7 +15,7 @@ import useWallet from '@/hooks/wallets/useWallet' import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -const keepPathRoutes = [AppRoutes.welcome, AppRoutes.newSafe.create, AppRoutes.newSafe.load] +const keepPathRoutes = [AppRoutes.welcome.index, AppRoutes.newSafe.create, AppRoutes.newSafe.load] const MenuWithTooltip = forwardRef(function MenuWithTooltip(props: any, ref) { return ( diff --git a/src/components/common/SafeLoadingError/index.tsx b/src/components/common/SafeLoadingError/index.tsx index a26fadf719..a9c17acf27 100644 --- a/src/components/common/SafeLoadingError/index.tsx +++ b/src/components/common/SafeLoadingError/index.tsx @@ -15,7 +15,7 @@ const SafeLoadingError = ({ children }: { children: ReactNode }): ReactElement = img={A vault with a red icon in the bottom right corner} text="This Safe Account couldn't be loaded" > - + diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index 310b09fbf7..a9ee033841 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -36,7 +36,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) expect(mockSetStep).not.toHaveBeenCalled() - expect(mockPushRoute).toHaveBeenCalledWith({ pathname: AppRoutes.welcome, query: undefined }) + expect(mockPushRoute).toHaveBeenCalledWith({ pathname: AppRoutes.welcome.index, query: undefined }) }) it('should go to the fourth step if there is a pending safe', async () => { diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index 1cb5429301..0231027e5e 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -169,7 +169,7 @@ const CreateSafe = () => { const initialStep = isSocialLogin ? 2 : 0 const onClose = () => { - router.push(AppRoutes.welcome) + router.push(AppRoutes.welcome.index) } return ( diff --git a/src/components/new-safe/create/steps/SetNameStep/index.tsx b/src/components/new-safe/create/steps/SetNameStep/index.tsx index 898d3ddd84..e6efd1fcad 100644 --- a/src/components/new-safe/create/steps/SetNameStep/index.tsx +++ b/src/components/new-safe/create/steps/SetNameStep/index.tsx @@ -63,7 +63,7 @@ function SetNameStep({ const onCancel = () => { trackEvent(CREATE_SAFE_EVENTS.CANCEL_CREATE_SAFE_FORM) - router.push(AppRoutes.welcome) + router.push(AppRoutes.welcome.index) } const isDisabled = isWrongChain || !isValid diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 255ad37572..642b2ccd05 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -49,7 +49,7 @@ export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRender const onClose = useCallback(() => { setPendingSafe(undefined) - router.push(AppRoutes.welcome) + router.push(AppRoutes.welcome.index) }, [router, setPendingSafe]) const handleRetry = useCallback(() => { diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 9604114671..ef4294bee5 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -22,7 +22,7 @@ const useSyncSafeCreationStep = (setStep: StepRenderProps['setS // Jump to connect wallet step if there is no wallet and no pending Safe if (!wallet) { - router.push({ pathname: AppRoutes.welcome, query: router.query }) + router.push({ pathname: AppRoutes.welcome.index, query: router.query }) } // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe diff --git a/src/components/new-safe/load/index.tsx b/src/components/new-safe/load/index.tsx index 727d39a840..84c24c91ee 100644 --- a/src/components/new-safe/load/index.tsx +++ b/src/components/new-safe/load/index.tsx @@ -46,7 +46,7 @@ const LoadSafe = ({ initialData }: { initialData?: TxStepperProps { - router.push(AppRoutes.welcome) + router.push(AppRoutes.welcome.index) } const initialSafe = initialData ?? loadSafeDefaultData diff --git a/src/components/sidebar/SafeList/index.tsx b/src/components/sidebar/SafeList/index.tsx index 3689c7831a..c004462395 100644 --- a/src/components/sidebar/SafeList/index.tsx +++ b/src/components/sidebar/SafeList/index.tsx @@ -82,7 +82,7 @@ const SafeList = ({ closeDrawer }: { closeDrawer?: () => void }): ReactElement = const hasWallet = !!wallet const hasNoSafes = Object.keys(ownedSafes).length === 0 && Object.keys(addedSafes).length === 0 - const isWelcomePage = router.pathname === AppRoutes.welcome + const isWelcomePage = router.pathname === AppRoutes.welcome.index || router.pathname === AppRoutes.welcome.socialLogin const isSingleTxPage = router.pathname === AppRoutes.transactions.tx /** @@ -110,7 +110,7 @@ const SafeList = ({ closeDrawer }: { closeDrawer?: () => void }): ReactElement = {!isWelcomePage && ( @@ -136,7 +136,7 @@ const SafeList = ({ closeDrawer }: { closeDrawer?: () => void }): ReactElement = {!isWelcomePage ? ( - + {NO_SAFE_MESSAGE} ) : ( @@ -193,7 +193,7 @@ const SafeList = ({ closeDrawer }: { closeDrawer?: () => void }): ReactElement = {!addedSafeEntriesOnChain.length && !ownedSafesOnChain.length && ( {!isWelcomePage ? ( - + {NO_SAFE_MESSAGE} ) : ( diff --git a/src/components/sidebar/Sidebar/index.tsx b/src/components/sidebar/Sidebar/index.tsx index f25d8798d3..b8b46e9a5c 100644 --- a/src/components/sidebar/Sidebar/index.tsx +++ b/src/components/sidebar/Sidebar/index.tsx @@ -10,7 +10,7 @@ import SidebarFooter from '@/components/sidebar/SidebarFooter' import css from './styles.module.css' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' -import { DataWidget } from '@/components/welcome/DataWidget' +import { DataWidget } from '@/components/welcome/SafeListDrawer/DataWidget' const Sidebar = (): ReactElement => { const [isDrawerOpen, setIsDrawerOpen] = useState(false) diff --git a/src/components/welcome/NewSafe.tsx b/src/components/welcome/NewSafe.tsx index 409b495b16..46d51deb55 100644 --- a/src/components/welcome/NewSafe.tsx +++ b/src/components/welcome/NewSafe.tsx @@ -1,17 +1,10 @@ -import React, { useState } from 'react' -import { Accordion, AccordionSummary, Box, Drawer, Grid, IconButton, SvgIcon, Typography } from '@mui/material' -import SafeList from '@/components/sidebar/SafeList' +import React from 'react' +import { Box, Grid, SvgIcon, Typography } from '@mui/material' import css from './styles.module.css' import CheckFilled from '@/public/images/common/check-filled.svg' -import ChevronRightIcon from '@mui/icons-material/ChevronRight' -import { DataWidget } from '@/components/welcome/DataWidget' import WelcomeLogin from './WelcomeLogin' -import { useAppSelector } from '@/store' -import { selectTotalAdded } from '@/store/addedSafesSlice' - -import drawerCSS from '@/components/sidebar/Sidebar/styles.module.css' -import useOwnedSafes from '@/hooks/useOwnedSafes' +import SafeListDrawer from '@/components/welcome/SafeListDrawer' const BulletListItem = ({ text }: { text: string }) => (
  • @@ -23,26 +16,8 @@ const BulletListItem = ({ text }: { text: string }) => ( ) const NewSafe = () => { - const numberOfAddedSafes = useAppSelector(selectTotalAdded) - const ownedSafes = useOwnedSafes() - const numberOfOwnedSafes = Object.values(ownedSafes).reduce((acc, curr) => acc + curr.length, 0) - const totalNumberOfSafes = numberOfOwnedSafes + numberOfAddedSafes - - const [showSidebar, setShowSidebar] = useState(false) - - const closeSidebar = () => setShowSidebar(false) - return ( <> - -
    - - -
    - -
    -
    -
    @@ -50,24 +25,7 @@ const NewSafe = () => {
    - {totalNumberOfSafes > 0 ? ( - - - setShowSidebar(true)} expanded={false}> - - - - - - - My Safe Accounts ({totalNumberOfSafes}) - - - - - - - ) : null} + diff --git a/src/components/welcome/NewSafeSocial.tsx b/src/components/welcome/NewSafeSocial.tsx new file mode 100644 index 0000000000..26c1d1da77 --- /dev/null +++ b/src/components/welcome/NewSafeSocial.tsx @@ -0,0 +1,87 @@ +import React from 'react' +import { Box, Button, Grid, Typography } from '@mui/material' +import css from './styles.module.css' +import Link from 'next/link' + +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' +import WelcomeLogin from './WelcomeLogin' +import GnosisChainLogo from '@/public/images/common/gnosis-chain-logo.png' +import Image from 'next/image' +import SafeListDrawer from '@/components/welcome/SafeListDrawer' + +const BulletListItem = ({ text }: { text: string }) => ( +
  • + + {text} + +
  • +) + +const MarqueeItem = () => { + return ( +
  • + Free on + Gnosis Chain logo + Gnosis Chain +
  • + ) +} + +const NewSafeSocial = () => { + return ( + <> + + + + + +
    + + + + + + + Get started in 30 seconds with your Google Account + + +
      + + + +
    + + {/* TODO: Replace with actual link and possibly add tracking */} + + + +
    + +
    +
      + + + +
    + +
    +
    +
    +
    + + ) +} + +export default NewSafeSocial diff --git a/src/components/welcome/DataWidget.tsx b/src/components/welcome/SafeListDrawer/DataWidget.tsx similarity index 100% rename from src/components/welcome/DataWidget.tsx rename to src/components/welcome/SafeListDrawer/DataWidget.tsx diff --git a/src/components/welcome/SafeListDrawer/index.tsx b/src/components/welcome/SafeListDrawer/index.tsx new file mode 100644 index 0000000000..afa8f5418d --- /dev/null +++ b/src/components/welcome/SafeListDrawer/index.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react' +import SafeList from '@/components/sidebar/SafeList' +import { DataWidget } from '@/components/welcome/SafeListDrawer/DataWidget' +import { Button, Drawer, Typography } from '@mui/material' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' +import { useAppSelector } from '@/store' +import { selectTotalAdded } from '@/store/addedSafesSlice' +import useOwnedSafes from '@/hooks/useOwnedSafes' +import drawerCSS from '@/components/sidebar/Sidebar/styles.module.css' +import css from './styles.module.css' + +const SafeListDrawer = () => { + const numberOfAddedSafes = useAppSelector(selectTotalAdded) + const ownedSafes = useOwnedSafes() + const numberOfOwnedSafes = Object.values(ownedSafes).reduce((acc, curr) => acc + curr.length, 0) + const totalNumberOfSafes = numberOfOwnedSafes + numberOfAddedSafes + const [showSidebar, setShowSidebar] = useState(false) + + const closeSidebar = () => setShowSidebar(false) + + if (totalNumberOfSafes <= 0) { + return null + } + + return ( + <> + +
    + + +
    + +
    +
    +
    + + + ) +} + +export default SafeListDrawer diff --git a/src/components/welcome/SafeListDrawer/styles.module.css b/src/components/welcome/SafeListDrawer/styles.module.css new file mode 100644 index 0000000000..5aed51e96f --- /dev/null +++ b/src/components/welcome/SafeListDrawer/styles.module.css @@ -0,0 +1,33 @@ +.button { + padding: var(--space-2); +} + +.buttonText { + align-self: center; + margin: auto; + display: flex; + gap: var(--space-1); +} + +.fileIcon { + display: flex; + align-items: center; + padding: var(--space-1); + background-color: var(--color-background-main); +} + +.card { + padding: var(--space-3); + gap: var(--space-2); + display: flex; + flex-direction: column; +} + +.card :global .MuiCardHeader-root, +.card :global .MuiCardContent-root { + padding: 0; +} + +.cardHeader { + text-align: left; +} diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index 2b8c377b76..fe9fa566d5 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -1,22 +1,3 @@ -.accordion :global .MuiAccordionDetails-root > div > div:first-child { - display: none; -} - -.accordion { - border: 0; - height: 100%; -} - -.accordion :global .MuiAccordionSummary-root { - background: transparent !important; - padding: var(--space-2); - border-bottom: 1px solid var(--color-border-light); -} - -.accordion :global .MuiAccordionSummary-content { - margin: 0; -} - .sidebar { max-height: calc(100vh - var(--header-height) - var(--footer-height) - 8px); overflow-y: auto; @@ -39,52 +20,87 @@ margin-bottom: var(--space-2); } -.content { - background: linear-gradient(-90deg, #10ff84, #5fddff); - background-size: 200% 200%; - animation: gradient 15s ease infinite; - border-radius: 6px; - padding: var(--space-3); +.numberList { + margin: var(--space-5) 0; + padding: 0; + list-style: none; +} + +.numberList li { display: flex; - flex-direction: column; - justify-content: center; - gap: var(--space-2); - width: 100%; - height: 100%; + flex-direction: row; + gap: var(--space-1); + align-items: center; + margin-bottom: var(--space-2); + counter-increment: item; } -.checkIcon { - color: var(--color-static-main); +.numberList li:before { + margin-right: var(--space-1); + content: counter(item); + background: var(--color-static-main); + border-radius: 100%; + color: white; + width: 32px; + height: 32px; + line-height: 32px; + font-size: 18px; + text-align: center; + flex-shrink: 0; } -.createAddCard { +.marquee { display: flex; - flex-direction: column; - padding: var(--space-4); - height: 100%; + overflow: hidden; + user-select: none; + gap: var(--space-6); + background-color: var(--color-static-main); + color: white; + padding: 20px; + margin: var(--space-3) calc(-1 * var(--space-9)) calc(-1 * var(--space-3)); } -.fileIcon { +.marqueeContent { + margin: 0; + padding: 0; + flex-shrink: 0; + display: flex; + justify-content: space-around; + min-width: 100%; + gap: var(--space-6); + animation: scroll 10s linear infinite; +} + +.marqueeItem { + font-size: 18px; display: flex; align-items: center; - padding: var(--space-1); - background-color: var(--color-background-main); } -.card { - padding: var(--space-3); - gap: var(--space-2); +.marqueeItem img { + margin: 0 var(--space-1); +} + +.content { + background: linear-gradient(-90deg, #10ff84, #5fddff); + background-size: 200% 200%; + animation: gradient 15s ease infinite; + border-radius: 6px; + padding: var(--space-9); display: flex; flex-direction: column; + justify-content: center; + gap: var(--space-2); + width: 100%; + height: 100%; } -.card :global .MuiCardHeader-root, -.card :global .MuiCardContent-root { - padding: 0; +.checkIcon { + color: var(--color-static-main); } -.cardHeader { - text-align: left; +.button { + color: var(--color-static-main); } @media (max-width: 899.95px) { @@ -92,23 +108,23 @@ height: 100%; } - .accordion { - height: auto; - } - - .accordion :global .MuiAccordionSummary-expandIconWrapper { - display: block; - } - .content { padding: var(--space-6); } + + .marquee { + margin: var(--space-3) calc(-1 * var(--space-6)) calc(-1 * var(--space-3)); + } } @media (max-width: 599.95px) { .content { padding: var(--space-4); } + + .marquee { + margin: var(--space-3) calc(-1 * var(--space-4)) 0; + } } @keyframes gradient { @@ -122,3 +138,12 @@ background-position: 0 50%; } } + +@keyframes scroll { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--space-6))); + } +} diff --git a/src/config/routes.ts b/src/config/routes.ts index 34b6848e00..9ea85b1e18 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,6 +1,5 @@ export const AppRoutes = { '404': '/404', - welcome: '/welcome', terms: '/terms', privacy: '/privacy', licenses: '/licenses', @@ -50,4 +49,8 @@ export const AppRoutes = { index: '/transactions', history: '/transactions/history', }, + welcome: { + socialLogin: '/welcome/social-login', + index: '/welcome', + }, } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a2c6442367..738a591942 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -21,8 +21,8 @@ const IndexPage: NextPage = () => { safeAddress ? `${AppRoutes.home}?safe=${safeAddress}` : chain - ? `${AppRoutes.welcome}?chain=${chain}` - : AppRoutes.welcome, + ? `${AppRoutes.welcome.index}?chain=${chain}` + : AppRoutes.welcome.index, ) }, [router, safeAddress, chain]) diff --git a/src/pages/welcome.tsx b/src/pages/welcome/index.tsx similarity index 100% rename from src/pages/welcome.tsx rename to src/pages/welcome/index.tsx diff --git a/src/pages/welcome/social-login.tsx b/src/pages/welcome/social-login.tsx new file mode 100644 index 0000000000..d4cf360333 --- /dev/null +++ b/src/pages/welcome/social-login.tsx @@ -0,0 +1,17 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import NewSafeSocial from '@/components/welcome/NewSafeSocial' + +const SocialLogin: NextPage = () => { + return ( + <> + + {'Safe{Wallet} – Welcome'} + + + + + ) +} + +export default SocialLogin From 0c47aeeb0d9a90cbdf43e55f8532a99d97cd0eab Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 31 Oct 2023 15:46:44 +0100 Subject: [PATCH 30/52] chore: Revert permission changes --- .github/workflows/lint.yml | 1 - .github/workflows/test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bc77009b65..d9bf694363 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,6 @@ concurrency: jobs: eslint: - permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a1dd52e3c..a25c47ab2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ concurrency: jobs: test: - permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From 915ef375d5a05f0d30b850f284a50be32a73be64 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 31 Oct 2023 15:55:13 +0100 Subject: [PATCH 31/52] fix: disconnect previous wallet (#2712) --- .../wallets/__tests__/useOnboard.test.ts | 254 ++++++++++++++++++ src/hooks/wallets/useOnboard.test.ts | 69 ----- src/hooks/wallets/useOnboard.ts | 16 +- 3 files changed, 268 insertions(+), 71 deletions(-) create mode 100644 src/hooks/wallets/__tests__/useOnboard.test.ts delete mode 100644 src/hooks/wallets/useOnboard.test.ts diff --git a/src/hooks/wallets/__tests__/useOnboard.test.ts b/src/hooks/wallets/__tests__/useOnboard.test.ts new file mode 100644 index 0000000000..7364078c7c --- /dev/null +++ b/src/hooks/wallets/__tests__/useOnboard.test.ts @@ -0,0 +1,254 @@ +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { faker } from '@faker-js/faker' +import type { EIP1193Provider, OnboardAPI, WalletState } from '@web3-onboard/core' +import { getConnectedWallet, switchWallet } from '../useOnboard' + +// mock wallets +jest.mock('@/hooks/wallets/wallets', () => ({ + getDefaultWallets: jest.fn(() => []), + getRecommendedInjectedWallets: jest.fn(() => []), +})) + +describe('useOnboard', () => { + describe('getConnectedWallet', () => { + it('returns the connected wallet', () => { + const wallets = [ + { + label: 'Wallet 1', + icon: 'wallet1.svg', + provider: null as unknown as EIP1193Provider, + chains: [{ id: '0x4' }], + accounts: [ + { + address: '0x1234567890123456789012345678901234567890', + ens: null, + balance: null, + }, + ], + }, + { + label: 'Wallet 2', + icon: 'wallet2.svg', + provider: null as unknown as EIP1193Provider, + chains: [{ id: '0x100' }], + accounts: [ + { + address: '0x2', + ens: null, + balance: null, + }, + ], + }, + ] as WalletState[] + + expect(getConnectedWallet(wallets)).toEqual({ + label: 'Wallet 1', + icon: 'wallet1.svg', + address: '0x1234567890123456789012345678901234567890', + provider: wallets[0].provider, + chainId: '4', + }) + }) + + it('should return null if the address is invalid', () => { + const wallets = [ + { + label: 'Wallet 1', + icon: 'wallet1.svg', + provider: null as unknown as EIP1193Provider, + chains: [{ id: '0x4' }], + accounts: [ + { + address: '0xinvalid', + ens: null, + balance: null, + }, + ], + }, + ] as WalletState[] + + expect(getConnectedWallet(wallets)).toBeNull() + }) + }) + + describe('switchWallet', () => { + it('should keep the social signer wallet connected if switching wallets fails', async () => { + const mockOnboard = { + state: { + get: jest.fn().mockReturnValue({ + wallets: [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: ONBOARD_MPC_MODULE_LABEL, + }, + ], + }), + }, + connectWallet: jest.fn().mockRejectedValue('Error'), + disconnectWallet: jest.fn(), + } + + await switchWallet(mockOnboard as unknown as OnboardAPI) + + expect(mockOnboard.connectWallet).toHaveBeenCalled() + expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() + }) + + it('should not disconnect the social signer wallet label if the same label connects', async () => { + const mockNewState = [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: ONBOARD_MPC_MODULE_LABEL, + }, + ] + + const mockOnboard = { + state: { + get: jest.fn().mockReturnValue({ + wallets: [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: ONBOARD_MPC_MODULE_LABEL, + }, + ], + }), + }, + connectWallet: jest.fn().mockResolvedValue(mockNewState), + disconnectWallet: jest.fn(), + } + + await switchWallet(mockOnboard as unknown as OnboardAPI) + + expect(mockOnboard.connectWallet).toHaveBeenCalled() + expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() + }) + + it('should not disconnect non social signer wallets if new wallet connects', async () => { + const mockNewState = [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: 'MetaMask', + }, + ] + + const mockOnboard = { + state: { + get: jest.fn().mockReturnValue({ + wallets: [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: 'Wallet Connect', + }, + ], + }), + }, + connectWallet: jest.fn().mockResolvedValue(mockNewState), + disconnectWallet: jest.fn(), + } + + await switchWallet(mockOnboard as unknown as OnboardAPI) + + expect(mockOnboard.connectWallet).toBeCalled() + expect(mockOnboard.disconnectWallet).not.toHaveBeenCalled() + }) + + it('should disconnect the social signer wallet label if new wallet connects', async () => { + const mockNewState = [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: 'MetaMask', + }, + ] + + const mockOnboard = { + state: { + get: jest.fn().mockReturnValue({ + wallets: [ + { + accounts: [ + { + address: faker.finance.ethereumAddress(), + ens: undefined, + }, + ], + chains: [ + { + id: '5', + }, + ], + label: ONBOARD_MPC_MODULE_LABEL, + }, + ], + }), + }, + connectWallet: jest.fn().mockResolvedValue(mockNewState), + disconnectWallet: jest.fn(), + } + + await switchWallet(mockOnboard as unknown as OnboardAPI) + + expect(mockOnboard.connectWallet).toBeCalled() + expect(mockOnboard.disconnectWallet).toHaveBeenCalledWith({ label: ONBOARD_MPC_MODULE_LABEL }) + }) + }) +}) diff --git a/src/hooks/wallets/useOnboard.test.ts b/src/hooks/wallets/useOnboard.test.ts deleted file mode 100644 index ed9c637574..0000000000 --- a/src/hooks/wallets/useOnboard.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { EIP1193Provider, WalletState } from '@web3-onboard/core' -import { getConnectedWallet } from './useOnboard' - -// mock wallets -jest.mock('@/hooks/wallets/wallets', () => ({ - getDefaultWallets: jest.fn(() => []), - getRecommendedInjectedWallets: jest.fn(() => []), -})) - -describe('getConnectedWallet', () => { - it('returns the connected wallet', () => { - const wallets = [ - { - label: 'Wallet 1', - icon: 'wallet1.svg', - provider: null as unknown as EIP1193Provider, - chains: [{ id: '0x4' }], - accounts: [ - { - address: '0x1234567890123456789012345678901234567890', - ens: null, - balance: null, - }, - ], - }, - { - label: 'Wallet 2', - icon: 'wallet2.svg', - provider: null as unknown as EIP1193Provider, - chains: [{ id: '0x100' }], - accounts: [ - { - address: '0x2', - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[] - - expect(getConnectedWallet(wallets)).toEqual({ - label: 'Wallet 1', - icon: 'wallet1.svg', - address: '0x1234567890123456789012345678901234567890', - provider: wallets[0].provider, - chainId: '4', - }) - }) - - it('should return null if the address is invalid', () => { - const wallets = [ - { - label: 'Wallet 1', - icon: 'wallet1.svg', - provider: null as unknown as EIP1193Provider, - chains: [{ id: '0x4' }], - accounts: [ - { - address: '0xinvalid', - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[] - - expect(getConnectedWallet(wallets)).toBeNull() - }) -}) diff --git a/src/hooks/wallets/useOnboard.ts b/src/hooks/wallets/useOnboard.ts index 2781d67d86..4d8f2c1ab9 100644 --- a/src/hooks/wallets/useOnboard.ts +++ b/src/hooks/wallets/useOnboard.ts @@ -10,6 +10,7 @@ import { useInitPairing } from '@/services/pairing/hooks' import { useAppSelector } from '@/store' import { type EnvState, selectRpc } from '@/store/settingsSlice' import { E2E_WALLET_NAME } from '@/tests/e2e-wallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' const WALLETCONNECT = 'WalletConnect' @@ -130,8 +131,19 @@ export const connectWallet = async ( return wallets } -export const switchWallet = (onboard: OnboardAPI) => { - connectWallet(onboard) +export const switchWallet = async (onboard: OnboardAPI) => { + const oldWalletLabel = getConnectedWallet(onboard.state.get().wallets)?.label + const newWallets = await connectWallet(onboard) + const newWalletLabel = newWallets ? getConnectedWallet(newWallets)?.label : undefined + + // If the wallet actually changed we disconnect the old connected wallet. + if (!newWalletLabel || oldWalletLabel !== ONBOARD_MPC_MODULE_LABEL) { + return + } + + if (newWalletLabel !== oldWalletLabel) { + await onboard.disconnectWallet({ label: oldWalletLabel }) + } } // Disable/enable wallets according to chain From 7093860776aa1f431601caa5556661ea4088e842 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 31 Oct 2023 15:55:56 +0100 Subject: [PATCH 32/52] fix: ignore empty names in addressbook (#2709) --- src/store/__tests__/addressBookSlice.test.ts | 12 ++++++++++++ src/store/addressBookSlice.ts | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/store/__tests__/addressBookSlice.test.ts b/src/store/__tests__/addressBookSlice.test.ts index 435927eafd..d72cc69d2b 100644 --- a/src/store/__tests__/addressBookSlice.test.ts +++ b/src/store/__tests__/addressBookSlice.test.ts @@ -32,6 +32,18 @@ describe('addressBookSlice', () => { }) }) + it('should ignore empty names in the address book', () => { + const state = addressBookSlice.reducer( + initialState, + upsertAddressBookEntry({ + chainId: '1', + address: '0x2', + name: '', + }), + ) + expect(state).toEqual(initialState) + }) + it('should edit an entry in the address book', () => { const state = addressBookSlice.reducer( initialState, diff --git a/src/store/addressBookSlice.ts b/src/store/addressBookSlice.ts index 62bf1f6449..9a24c03637 100644 --- a/src/store/addressBookSlice.ts +++ b/src/store/addressBookSlice.ts @@ -26,6 +26,9 @@ export const addressBookSlice = createSlice({ upsertAddressBookEntry: (state, action: PayloadAction<{ chainId: string; address: string; name: string }>) => { const { chainId, address, name } = action.payload + if (name.trim() === '') { + return + } if (!state[chainId]) state[chainId] = {} state[chainId][address] = name }, From 52366b31ee8c5c35a91879d6551280e131a30352 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 1 Nov 2023 14:07:07 +0100 Subject: [PATCH 33/52] feat: aggregate verifier config (#2724) --- .env.example | 8 +++++++- .github/workflows/build/action.yml | 3 ++- .../WelcomeLogin/__tests__/WalletLogin.test.tsx | 2 ++ src/config/constants.ts | 4 ++-- src/services/mpc/SocialWalletService.ts | 16 ++++++++++------ 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 6642c2c52c..a65de784e2 100644 --- a/.env.example +++ b/.env.example @@ -40,4 +40,10 @@ NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING= NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= # Redefine -NEXT_PUBLIC_REDEFINE_API= \ No newline at end of file +NEXT_PUBLIC_REDEFINE_API= + +# Social Login +NEXT_PUBLIC_WEB3AUTH_CLIENT_ID= +NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID= +NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID= +NEXT_PUBLIC_GOOGLE_CLIENT_ID= \ No newline at end of file diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 466b80b390..ecde666f8e 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -44,8 +44,9 @@ runs: NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} NEXT_PUBLIC_WEB3AUTH_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_CLIENT_ID }} + NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID }} + NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID }} NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_CLIENT_ID }} - NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID }} NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx index 2f28d230f1..4a4feb1ea9 100644 --- a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -71,6 +71,8 @@ describe('WalletLogin', () => { it('should not invoke the callback if connection fails', async () => { const mockOnLogin = jest.fn() + jest.spyOn(useWallet, 'default').mockReturnValue(null) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn().mockReturnValue([])) const result = render() diff --git a/src/config/constants.ts b/src/config/constants.ts index 6f1b3f52d0..7fe1a627b8 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -102,5 +102,5 @@ export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FI // Social Login export const WEB3_AUTH_CLIENT_ID = process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID || '' export const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '' -export const WEB3AUTH_VERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_VERIFIER_ID || '' -export const AUTHENTICATOR_APP_VERIFIER = process.env.NEXT_PUBLIC_AUTHENTICATOR_APP_VERIFIER || '' +export const WEB3AUTH_SUBVERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID || '' +export const WEB3AUTH_AGGREGATE_VERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID || '' diff --git a/src/services/mpc/SocialWalletService.ts b/src/services/mpc/SocialWalletService.ts index 3fa00d7a7e..d6972115de 100644 --- a/src/services/mpc/SocialWalletService.ts +++ b/src/services/mpc/SocialWalletService.ts @@ -1,6 +1,6 @@ import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import BN from 'bn.js' -import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' +import { GOOGLE_CLIENT_ID, WEB3AUTH_AGGREGATE_VERIFIER_ID, WEB3AUTH_SUBVERIFIER_ID } from '@/config/constants' import { SecurityQuestionRecovery } from '@/services/mpc/recovery/SecurityQuestionRecovery' import { trackEvent } from '@/services/analytics' import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' @@ -69,11 +69,15 @@ class SocialWalletService implements ISocialWalletService { async loginAndCreate(): Promise { try { await this.mpcCoreKit.loginWithOauth({ - subVerifierDetails: { - typeOfLogin: 'google', - verifier: WEB3AUTH_VERIFIER_ID, - clientId: GOOGLE_CLIENT_ID, - }, + aggregateVerifierIdentifier: WEB3AUTH_AGGREGATE_VERIFIER_ID, + subVerifierDetailsArray: [ + { + clientId: GOOGLE_CLIENT_ID, + typeOfLogin: 'google', + verifier: WEB3AUTH_SUBVERIFIER_ID, + }, + ], + aggregateVerifierType: 'single_id_verifier', }) if (this.mpcCoreKit.status === COREKIT_STATUS.REQUIRED_SHARE) { From f262276711157cb6eebdbe5d98eddc8e41f67dce Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:36:13 +0100 Subject: [PATCH 34/52] [Seedless Onboarding] QA fixes (#2721) * fix: Adjust tooltip wording * fix: Hide overview widget in the review screen * fix: Don't redirect the user to welcome if there is a pending safe * fix: Add loading state to notification enableAll * fix: Show Security&Login tab in global preferences view * fix: Show an orange badge if mfa is not enabled * fix: Revert hook changes, adjust test case * fix: failing tests * fix: Rename socialLogin to socialSigner --- .../common/ConnectWallet/ConnectionCenter.tsx | 3 --- .../common/ConnectWallet/styles.module.css | 1 + src/components/common/SocialLoginInfo/index.tsx | 7 +++++-- .../common/SocialLoginInfo/styles.module.css | 16 ++++++++++++++-- .../__tests__/SocialSignerLogin.test.tsx | 8 +++++--- src/components/common/SocialSigner/index.tsx | 2 +- .../common/WalletOverview/styles.module.css | 2 +- .../__tests__/useSyncSafeCreationStep.test.ts | 9 +++++++-- src/components/new-safe/create/index.tsx | 2 +- .../new-safe/create/useSyncSafeCreationStep.ts | 2 +- .../PushNotificationsBanner/index.tsx | 11 +++++++++-- .../sidebar/SidebarNavigation/config.tsx | 4 ++++ .../welcome/WelcomeLogin/WalletLogin.tsx | 2 +- src/hooks/wallets/consts.ts | 2 +- 14 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx index 301f3b1286..6ff98250c5 100644 --- a/src/components/common/ConnectWallet/ConnectionCenter.tsx +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -7,11 +7,8 @@ import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' import css from '@/components/common/ConnectWallet/styles.module.css' -import { useCurrentChain } from '@/hooks/useChains' const ConnectionCenter = (): ReactElement => { - const chain = useCurrentChain() - const [anchorEl, setAnchorEl] = useState(null) const open = !!anchorEl diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index dd7bf01c3a..a4b65433e6 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -8,6 +8,7 @@ align-items: center; text-align: left; gap: var(--space-1); + padding: 0 var(--space-2); } .popoverContainer { diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx index 4329cc2c83..42c2d3a0c9 100644 --- a/src/components/common/SocialLoginInfo/index.tsx +++ b/src/components/common/SocialLoginInfo/index.tsx @@ -1,4 +1,4 @@ -import { Box, Typography } from '@mui/material' +import { Badge, Box, Typography } from '@mui/material' import css from './styles.module.css' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type ConnectedWallet } from '@/services/onboard' @@ -28,7 +28,10 @@ const SocialLoginInfo = ({ return ( - Profile Image + + Profile Image + {!socialWalletService?.isMFAEnabled() && } +
    {userInfo.name} diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css index 6b694addb6..c159474142 100644 --- a/src/components/common/SocialLoginInfo/styles.module.css +++ b/src/components/common/SocialLoginInfo/styles.module.css @@ -1,7 +1,19 @@ .profileImg { border-radius: var(--space-2); - width: 32px; - height: 32px; + width: 28px; + height: 28px; + display: block; +} + +.bubble { + content: ''; + position: absolute; + right: 3px; + bottom: 3px; +} + +.bubble span { + outline: 1px solid var(--color-background-paper); } .profileData { diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx index 29a8ab560d..44d81351fc 100644 --- a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx +++ b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx @@ -94,7 +94,9 @@ describe('SocialSignerLogin', () => { />, ) - expect(result.getByText('Currently only supported on Goerli')).toBeInTheDocument() + expect( + result.getByText('Currently only supported on Goerli. More network support coming soon.'), + ).toBeInTheDocument() expect(await result.findByRole('button')).toBeDisabled() }) @@ -150,7 +152,7 @@ describe('SocialSignerLogin', () => { .with({ chainId: '1', chainName: 'Ethereum', - disabledWallets: ['socialLogin'], + disabledWallets: ['socialSigner'], }) .build() const mockGnosisChain = chainBuilder() @@ -167,7 +169,7 @@ describe('SocialSignerLogin', () => { it('returns an empty array if social login is not enabled on any chain', () => { const mockGoerliChain = chainBuilder() - .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialLogin'] }) + .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialSigner'] }) .build() const mockChains = [mockEthereumChain, mockGoerliChain] diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index f230e3ec57..8b14e58160 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -166,7 +166,7 @@ export const SocialSigner = ({ ml: 0.5, }} /> - Currently only supported on {supportedChains.join(', ')} + Currently only supported on {supportedChains.join(', ')}. More network support coming soon. )} diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css index 96ddc9f7b5..8316ab25da 100644 --- a/src/components/common/WalletOverview/styles.module.css +++ b/src/components/common/WalletOverview/styles.module.css @@ -32,7 +32,7 @@ display: none; } - .socialLoginInfo > div > div { + .socialLoginInfo > div > div:last-child { display: none; } } diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index a9ee033841..64eea7d305 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -21,10 +21,9 @@ describe('useSyncSafeCreationStep', () => { beforeEach(() => { jest.clearAllMocks() - const setPendingSafeSpy = jest.fn() }) - it('should go to the first step if no wallet is connected', async () => { + it('should go to the first step if no wallet is connected and there is no pending safe', async () => { const mockPushRoute = jest.fn() jest.spyOn(wallet, 'default').mockReturnValue(null) jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) @@ -40,15 +39,21 @@ describe('useSyncSafeCreationStep', () => { }) it('should go to the fourth step if there is a pending safe', async () => { + const mockPushRoute = jest.fn() jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, setPendingSafeSpy]) + jest.spyOn(useRouter, 'useRouter').mockReturnValue({ + push: mockPushRoute, + } as unknown as NextRouter) const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) expect(mockSetStep).toHaveBeenCalledWith(3) + + expect(mockPushRoute).not.toHaveBeenCalled() }) it('should go to the second step if the wrong chain is connected', async () => { diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index 0231027e5e..b71c63564d 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -193,7 +193,7 @@ const CreateSafe = () => { - {activeStep < 3 && } + {activeStep < 2 && } {wallet?.address && } diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index ef4294bee5..90a32c0e7f 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -20,7 +20,7 @@ const useSyncSafeCreationStep = (setStep: StepRenderProps['setS return } - // Jump to connect wallet step if there is no wallet and no pending Safe + // Jump to the welcome page if there is no wallet if (!wallet) { router.push({ pathname: AppRoutes.welcome.index, query: router.query }) } diff --git a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx index 0b1b64106c..9bf3c4cff8 100644 --- a/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx +++ b/src/components/settings/PushNotifications/PushNotificationsBanner/index.tsx @@ -1,7 +1,7 @@ import { Button, Chip, Grid, SvgIcon, Typography, IconButton } from '@mui/material' import Link from 'next/link' import { useRouter } from 'next/router' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import type { ReactElement } from 'react' import { CustomTooltip } from '@/components/common/CustomTooltip' @@ -26,6 +26,7 @@ import type { AddedSafesOnChain } from '@/store/addedSafesSlice' import type { PushNotificationPreferences } from '@/services/push-notifications/preferences' import type { NotifiableSafes } from '../logic' import useWallet from '@/hooks/wallets/useWallet' +import CircularProgress from '@mui/material/CircularProgress' import css from './styles.module.css' @@ -103,6 +104,7 @@ const TrackBanner = (): null => { } export const PushNotificationsBanner = ({ children }: { children: ReactElement }): ReactElement => { + const [isSubmitting, setIsSubmitting] = useState(false) const isNotificationFeatureEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const chain = useCurrentChain() const totalAddedSafes = useAppSelector(selectTotalAdded) @@ -136,6 +138,8 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } return } + setIsSubmitting(true) + trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_ALL) const allPreferences = getAllPreferences() @@ -144,11 +148,13 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } try { await assertWalletChain(onboard, safe.chainId) } catch { + setIsSubmitting(false) return } await registerNotifications(safesToRegister) + setIsSubmitting(false) dismissBanner() } @@ -195,7 +201,8 @@ export const PushNotificationsBanner = ({ children }: { children: ReactElement } size="small" className={css.button} onClick={onEnableAll} - disabled={!isOk || !onboard} + startIcon={isSubmitting ? : null} + disabled={!isOk || !onboard || isSubmitting} > Enable all diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 4686b6c2ba..359ec63826 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -127,6 +127,10 @@ export const generalSettingsNavItems = [ label: 'Notifications', href: AppRoutes.settings.notifications, }, + { + label: 'Security & Login', + href: AppRoutes.settings.securityLogin, + }, { label: 'Data', href: AppRoutes.settings.data, diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 54242c585f..21674d16a8 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -23,7 +23,7 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { if (wallet !== null && !isSocialLogin) { return ( - +
    } /> From 53d95cde80541084ec03d26c8cdcf09e88e506bb Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 3 Nov 2023 10:45:25 +0000 Subject: [PATCH 39/52] refactor: environment config for social wallet (#2728) * refactor: environment config for social wallet * refactor: Use IS_PRODUCTION directly, adjust isSocialWalletOptions * fix: Failing tests * fix: Adjust error message --------- Co-authored-by: Usame Algan --- .env.example | 7 ++-- src/hooks/useMnemonicName/dict.ts | 11 +---- .../wallets/mpc/__tests__/useMPC.test.ts | 2 + src/hooks/wallets/mpc/useMPC.ts | 7 ++-- src/services/mpc/SocialWalletService.ts | 13 ++++-- .../mpc/__tests__/SocialWalletService.test.ts | 2 + src/services/mpc/config.ts | 40 +++++++++++++++++++ 7 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 src/services/mpc/config.ts diff --git a/.env.example b/.env.example index a65de784e2..4ee43536d7 100644 --- a/.env.example +++ b/.env.example @@ -43,7 +43,6 @@ NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= NEXT_PUBLIC_REDEFINE_API= # Social Login -NEXT_PUBLIC_WEB3AUTH_CLIENT_ID= -NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID= -NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID= -NEXT_PUBLIC_GOOGLE_CLIENT_ID= \ No newline at end of file +NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING= +NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION= + diff --git a/src/hooks/useMnemonicName/dict.ts b/src/hooks/useMnemonicName/dict.ts index 7e6608315e..d45be71e76 100644 --- a/src/hooks/useMnemonicName/dict.ts +++ b/src/hooks/useMnemonicName/dict.ts @@ -101,7 +101,6 @@ goshawk grasshopper grouse guanaco -guinea-pig gull hamster hare @@ -130,7 +129,6 @@ kangaroo kingfisher kinkajou koala -komodo-dragon kookaburra kouprey kudu @@ -188,7 +186,6 @@ pheasant pig pigeon pika -polar-bear pony porcupine porpoise @@ -201,18 +198,15 @@ raccoon ram rat raven -red-deer -red-panda reindeer rhea rhinoceros rook salamander salmon -sand-dollar +sand sandpiper sardine -sea-lion seahorse seal shark @@ -278,7 +272,6 @@ excellent merry amiable exceptional -mild-mannered amused excited nice @@ -322,10 +315,8 @@ calm good proud charming -good-humored relaxed cheerful -good-natured reliable cheery gracious diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index a35c4875c0..2cdfd3f689 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -1,4 +1,5 @@ import * as useOnboard from '@/hooks/wallets/useOnboard' +import * as socialWalletOptions from '@/services/mpc/config' import { renderHook, waitFor } from '@/tests/test-utils' import { _getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' import * as useChains from '@/hooks/useChains' @@ -65,6 +66,7 @@ class EventEmittingMockProvider { describe('useInitMPC', () => { beforeEach(() => { jest.resetAllMocks() + jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) }) it('should set the coreKit if user is not logged in yet', async () => { const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index 611dede8d5..ef580a0f14 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -3,12 +3,12 @@ import ExternalStore from '@/services/ExternalStore' import { COREKIT_STATUS, Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' import { CHAIN_NAMESPACES } from '@web3auth/base' -import { WEB3_AUTH_CLIENT_ID } from '@/config/constants' import { useCurrentChain } from '@/hooks/useChains' import { getRpcServiceUrl } from '../web3' import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' import { useInitSocialWallet } from './useSocialWallet' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' const { getStore, setStore, useStore } = new ExternalStore() @@ -18,9 +18,10 @@ export const useInitMPC = () => { useInitSocialWallet() useEffect(() => { - if (!chain || !onboard) { + if (!chain || !onboard || !isSocialWalletOptions(SOCIAL_WALLET_OPTIONS)) { return } + const chainConfig = { chainId: `0x${Number(chain.chainId).toString(16)}`, chainNamespace: CHAIN_NAMESPACES.EIP155, @@ -40,7 +41,7 @@ export const useInitMPC = () => { } const web3AuthCoreKit = new Web3AuthMPCCoreKit({ - web3AuthClientId: WEB3_AUTH_CLIENT_ID, + web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, // Available networks are "sapphire_devnet", "sapphire_mainnet" web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, baseUrl: `${window.location.origin}/serviceworker`, diff --git a/src/services/mpc/SocialWalletService.ts b/src/services/mpc/SocialWalletService.ts index 964d7c8419..5f70963f2f 100644 --- a/src/services/mpc/SocialWalletService.ts +++ b/src/services/mpc/SocialWalletService.ts @@ -1,6 +1,5 @@ import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import BN from 'bn.js' -import { GOOGLE_CLIENT_ID, WEB3AUTH_AGGREGATE_VERIFIER_ID, WEB3AUTH_SUBVERIFIER_ID } from '@/config/constants' import { SecurityQuestionRecovery } from '@/services/mpc/recovery/SecurityQuestionRecovery' import { trackEvent } from '@/services/analytics' import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' @@ -9,6 +8,7 @@ import { logError } from '../exceptions' import ErrorCodes from '../exceptions/ErrorCodes' import { asError } from '../exceptions/utils' import { type ISocialWalletService } from './interfaces' +import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from './config' /** * Singleton Service for accessing the social login wallet @@ -67,14 +67,19 @@ class SocialWalletService implements ISocialWalletService { } async loginAndCreate(): Promise { + const config = SOCIAL_WALLET_OPTIONS + const isConfigured = isSocialWalletOptions(config) + if (!isConfigured) { + throw new Error('The Social signer wallet is not configured correctly') + } try { await this.mpcCoreKit.loginWithOauth({ - aggregateVerifierIdentifier: WEB3AUTH_AGGREGATE_VERIFIER_ID, + aggregateVerifierIdentifier: config.web3AuthAggregateVerifierId, subVerifierDetailsArray: [ { - clientId: GOOGLE_CLIENT_ID, + clientId: config.googleClientId, typeOfLogin: 'google', - verifier: WEB3AUTH_SUBVERIFIER_ID, + verifier: config.web3AuthSubverifierId, }, ], aggregateVerifierType: 'single_id_verifier', diff --git a/src/services/mpc/__tests__/SocialWalletService.test.ts b/src/services/mpc/__tests__/SocialWalletService.test.ts index e3163b1069..4e766b2556 100644 --- a/src/services/mpc/__tests__/SocialWalletService.test.ts +++ b/src/services/mpc/__tests__/SocialWalletService.test.ts @@ -7,6 +7,7 @@ import { type TssSecurityQuestion, } from '@web3auth/mpc-core-kit' import * as mpcCoreKit from '@web3auth/mpc-core-kit' +import * as socialWalletOptions from '@/services/mpc/config' import { ethers } from 'ethers' import BN from 'bn.js' import { hexZeroPad } from 'ethers/lib/utils' @@ -76,6 +77,7 @@ describe('useMPCWallet', () => { }) beforeEach(() => { jest.resetAllMocks() + jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) }) afterAll(() => { jest.useRealTimers() diff --git a/src/services/mpc/config.ts b/src/services/mpc/config.ts new file mode 100644 index 0000000000..17dce21cc2 --- /dev/null +++ b/src/services/mpc/config.ts @@ -0,0 +1,40 @@ +import { IS_PRODUCTION } from '@/config/constants' + +enum SocialWalletOptionsKeys { + web3AuthClientId = 'web3AuthClientId', + web3AuthAggregateVerifierId = 'web3AuthAggregateVerifierId', + web3AuthSubverifierId = 'web3AuthSubverifierId', + googleClientId = 'googleClientId', +} + +export type SocialWalletOptions = { + [SocialWalletOptionsKeys.web3AuthClientId]: string + [SocialWalletOptionsKeys.web3AuthAggregateVerifierId]: string + [SocialWalletOptionsKeys.web3AuthSubverifierId]: string + [SocialWalletOptionsKeys.googleClientId]: string +} + +export const isSocialWalletOptions = (options: unknown): options is SocialWalletOptions => { + if (typeof options !== 'object' || options === null) { + return false + } + + const requiredKeys = Object.values(SocialWalletOptionsKeys) + const hasRequiredKeys = requiredKeys.every((key) => key in options) + const hasValues = Object.values(options).every(Boolean) + + return hasRequiredKeys && hasValues +} + +/** env variables */ +export const SOCIAL_WALLET_OPTIONS: any = (() => { + const SOCIAL_WALLET_OPTIONS_PRODUCTION = process.env.NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION || '' + const SOCIAL_WALLET_OPTIONS_STAGING = process.env.NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING || '' + + try { + return JSON.parse(IS_PRODUCTION ? SOCIAL_WALLET_OPTIONS_PRODUCTION : SOCIAL_WALLET_OPTIONS_STAGING) + } catch (error) { + console.error('Error parsing SOCIAL_WALLET_OPTIONS', error) + return {} + } +})() From 13618c345a9a10d7e8103e62afb9cd3ef4bf0913 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:19:34 +0100 Subject: [PATCH 40/52] fix: Reload app when setting password (#2741) --- .../SecurityLogin/SocialSignerMFA/index.tsx | 13 ++++++++++++- .../SecurityLogin/SocialSignerMFA/styles.module.css | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx index bca00660bf..cfb4ec0464 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx @@ -13,6 +13,7 @@ import { Alert, } from '@mui/material' import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' +import { useRouter } from 'next/router' import { useState, useMemo, type ChangeEvent } from 'react' import { FormProvider, useForm } from 'react-hook-form' import CheckIcon from '@/public/images/common/check-filled.svg' @@ -78,6 +79,7 @@ const passwordStrengthMap = { } as const const SocialSignerMFA = () => { + const router = useRouter() const socialWalletService = useSocialWallet() const [passwordStrength, setPasswordStrength] = useState() const [submitError, setSubmitError] = useState() @@ -111,6 +113,9 @@ const SocialSignerMFA = () => { ) onReset() setOpen(false) + + // This is a workaround so that the isPasswordSet and isMFAEnabled state update + router.reload() } catch (e) { setSubmitError('The password you entered is incorrect. Please try again.') } @@ -257,7 +262,13 @@ const SocialSignerMFA = () => {
    - + `1px solid ${theme.palette.border.light}` }} + > diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css index e753abc7be..b41b19e58f 100644 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css +++ b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css @@ -16,7 +16,7 @@ content: counter(item); background: var(--color-primary-main); border-radius: 100%; - color: white; + color: var(--color-background-paper); width: 20px; height: 20px; line-height: 20px; From 3d118f31b078ead673c9713370b79ee31344f163 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:17:20 +0100 Subject: [PATCH 41/52] Fix: hide Social Login wallet button from onboard (#2740) * Fix: hide Social Login wallet button from onboard * Watch onboard modal * Move to useSocialWallet --- src/hooks/wallets/mpc/useSocialWallet.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts index d98998ffdd..4d2dd700ef 100644 --- a/src/hooks/wallets/mpc/useSocialWallet.ts +++ b/src/hooks/wallets/mpc/useSocialWallet.ts @@ -13,6 +13,23 @@ import useMpc from './useMPC' const { getStore, setStore, useStore } = new ExternalStore() +// Listen to onboard modal open and hide the social login button +const hideOnboardButton = () => { + const onboardRoot = document.querySelector('onboard-v2')?.shadowRoot + if (!onboardRoot) return + + const hideSocialLoginButton = () => { + const walletButtons = onboardRoot.querySelectorAll('.wallet-button-container') || [] + const socialButton = Array.from(walletButtons).find((el) => el.textContent?.includes(ONBOARD_MPC_MODULE_LABEL)) + socialButton?.remove() + } + + const observer = new MutationObserver(hideSocialLoginButton) + observer.observe(onboardRoot, { childList: true }) + + return () => observer.disconnect() +} + export const useInitSocialWallet = () => { const mpcCoreKit = useMpc() const onboard = useOnboard() @@ -54,6 +71,13 @@ export const useInitSocialWallet = () => { setStore(new SocialWalletService(mpcCoreKit)) } }, [mpcCoreKit]) + + // Hide social login when onboard pops up + // @FIXME the button should work but atm it doesn't + useEffect(() => { + if (!onboard) return + return hideOnboardButton() + }, [onboard]) } export const getSocialWalletService = getStore From f22a797af3e820e6204fd1ff9bbd858687ef0b42 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 3 Nov 2023 15:09:40 +0100 Subject: [PATCH 42/52] fix: Use github secrets for env --- .github/workflows/build/action.yml | 6 ++---- src/config/constants.ts | 6 ------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index ecde666f8e..8a8c32e0c1 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,10 +43,8 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} - NEXT_PUBLIC_WEB3AUTH_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_CLIENT_ID }} - NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID }} - NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID }} - NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_CLIENT_ID }} + NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING }} + NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} diff --git a/src/config/constants.ts b/src/config/constants.ts index 7fe1a627b8..7edd0dd2e1 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -98,9 +98,3 @@ export const IS_OFFICIAL_HOST = process.env.NEXT_PUBLIC_IS_OFFICIAL_HOST === 'tr export const REDEFINE_SIMULATION_URL = 'https://dashboard.redefine.net/reports/' export const REDEFINE_API = process.env.NEXT_PUBLIC_REDEFINE_API export const REDEFINE_ARTICLE = 'https://safe.mirror.xyz/rInLWZwD_sf7enjoFerj6FIzCYmVMGrrV8Nhg4THdwI' - -// Social Login -export const WEB3_AUTH_CLIENT_ID = process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID || '' -export const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '' -export const WEB3AUTH_SUBVERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_SUBVERIFIER_ID || '' -export const WEB3AUTH_AGGREGATE_VERIFIER_ID = process.env.NEXT_PUBLIC_WEB3AUTH_AGGREGATE_VERIFIER_ID || '' From 010faca5492cab6303ab6a717ef56a861a2a4cb5 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:25:49 +0100 Subject: [PATCH 43/52] fix: Add social login option to onboard module (#2744) --- src/hooks/wallets/mpc/useSocialWallet.ts | 24 ----------- src/pages/_app.tsx | 3 ++ src/services/mpc/PasswordRecoveryModal.tsx | 42 ++++++++++++++++++++ src/services/mpc/SocialLoginModule.ts | 46 ++++++++++++++++++---- 4 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 src/services/mpc/PasswordRecoveryModal.tsx diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts index 4d2dd700ef..d98998ffdd 100644 --- a/src/hooks/wallets/mpc/useSocialWallet.ts +++ b/src/hooks/wallets/mpc/useSocialWallet.ts @@ -13,23 +13,6 @@ import useMpc from './useMPC' const { getStore, setStore, useStore } = new ExternalStore() -// Listen to onboard modal open and hide the social login button -const hideOnboardButton = () => { - const onboardRoot = document.querySelector('onboard-v2')?.shadowRoot - if (!onboardRoot) return - - const hideSocialLoginButton = () => { - const walletButtons = onboardRoot.querySelectorAll('.wallet-button-container') || [] - const socialButton = Array.from(walletButtons).find((el) => el.textContent?.includes(ONBOARD_MPC_MODULE_LABEL)) - socialButton?.remove() - } - - const observer = new MutationObserver(hideSocialLoginButton) - observer.observe(onboardRoot, { childList: true }) - - return () => observer.disconnect() -} - export const useInitSocialWallet = () => { const mpcCoreKit = useMpc() const onboard = useOnboard() @@ -71,13 +54,6 @@ export const useInitSocialWallet = () => { setStore(new SocialWalletService(mpcCoreKit)) } }, [mpcCoreKit]) - - // Hide social login when onboard pops up - // @FIXME the button should work but atm it doesn't - useEffect(() => { - if (!onboard) return - return hideOnboardButton() - }, [onboard]) } export const getSocialWalletService = getStore diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ec90253c7a..e7b3d6d3d7 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,3 +1,4 @@ +import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import Sentry from '@/services/sentry' // needs to be imported first import type { ReactNode } from 'react' import { type ReactElement } from 'react' @@ -126,6 +127,8 @@ const WebCoreApp = ({ + + diff --git a/src/services/mpc/PasswordRecoveryModal.tsx b/src/services/mpc/PasswordRecoveryModal.tsx new file mode 100644 index 0000000000..7a7e1d08ab --- /dev/null +++ b/src/services/mpc/PasswordRecoveryModal.tsx @@ -0,0 +1,42 @@ +import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' +import TxModalDialog from '@/components/common/TxModalDialog' +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' +import ExternalStore from '@/services/ExternalStore' + +const { useStore: useCloseCallback, setStore: setCloseCallback } = new ExternalStore<() => void>() + +export const open = (cb: () => void) => { + setCloseCallback(() => cb) +} + +export const close = () => { + setCloseCallback(undefined) +} + +const PasswordRecoveryModal = () => { + const socialWalletService = useSocialWallet() + const closeCallback = useCloseCallback() + const open = !!closeCallback + + const handleClose = () => { + closeCallback?.() + setCloseCallback(undefined) + close() + } + + const recoverPassword = async (password: string, storeDeviceFactor: boolean) => { + if (!socialWalletService) return + + await socialWalletService.recoverAccountWithPassword(password, storeDeviceFactor) + } + + if (!open) return null + + return ( + + + + ) +} + +export default PasswordRecoveryModal diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts index f9921000b3..aeafa92bde 100644 --- a/src/services/mpc/SocialLoginModule.ts +++ b/src/services/mpc/SocialLoginModule.ts @@ -1,7 +1,10 @@ import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' +import { getSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' +import * as PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' import { type EIP1193Provider } from '@web3-onboard/core' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' const getMPCProvider = () => _getMPCCoreKitInstance()?.provider @@ -18,6 +21,18 @@ export const isSocialLoginWallet = (walletLabel: string | undefined) => { return walletLabel === ONBOARD_MPC_MODULE_LABEL } +const getConnectedAccounts = async () => { + try { + const web3 = assertDefined(getMPCProvider()) + return web3.request({ method: 'eth_accounts' }) + } catch (e) { + throw new ProviderRpcError({ + code: 4001, + message: 'Provider is unavailable', + }) + } +} + /** * Module for MPC wallet created by the Web3Auth tKey MPC. * We gain access to the provider created by tKey MPC after a successful login. @@ -37,26 +52,42 @@ function MpcModule(): WalletInit { web3.on(event, listener) }, request: (request) => { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { try { - const web3 = assertDefined(getMPCProvider()) - const web3ReadOnly = assertDefined(getWeb3ReadOnly()) /* * We have to fallback to web3ReadOnly for eth_estimateGas because the provider by Web3Auth does not expose / implement it. */ if ('eth_estimateGas' === request.method) { + const web3ReadOnly = assertDefined(getWeb3ReadOnly()) + web3ReadOnly ?.send(request.method, request.params ?? []) .then(resolve) .catch(reject) + return } - /* - * If the provider is defined we already have access to the accounts. So we can just reply with the current account. - */ if ('eth_requestAccounts' === request.method) { - web3.request({ method: 'eth_accounts' }).then(resolve).catch(reject) + try { + // If the provider is defined we already have access to the accounts. + const web3 = assertDefined(getMPCProvider()) + web3.request({ method: 'eth_accounts' }).then(resolve).catch(reject) + } catch (e) { + // Otherwise try to log in the user + const socialWalletService = getSocialWalletService() + if (!socialWalletService) throw Error('Social Login not ready') + + const status = await socialWalletService.loginAndCreate() + + if (status === COREKIT_STATUS.REQUIRED_SHARE) { + PasswordRecoveryModal.open(() => { + getConnectedAccounts().then(resolve).catch(reject) + }) + } else { + getConnectedAccounts().then(resolve).catch(reject) + } + } return } @@ -66,6 +97,7 @@ function MpcModule(): WalletInit { return } + const web3 = assertDefined(getMPCProvider()) // Default: we call the inner provider web3.request(request).then(resolve).catch(reject) return From 232674c73694fef6d50b9ad3ed1dc0fd32dd9332 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:49:34 +0100 Subject: [PATCH 44/52] fix: Adjust connection center design (#2745) * fix: Adjust connection center design * refactor: Use currentColor for the icon --- .../common/ConnectWallet/ConnectionCenter.tsx | 4 +-- .../common/ConnectWallet/WalletDetails.tsx | 8 +++-- .../common/ConnectWallet/styles.module.css | 4 +++ .../common/icons/KeyholeIcon/index.tsx | 31 ++++++++++++++++--- .../create/steps/ReviewStep/index.test.tsx | 1 + .../create/steps/ReviewStep/index.tsx | 2 +- src/components/sidebar/SafeList/index.tsx | 2 +- 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx index 6ff98250c5..8201241a16 100644 --- a/src/components/common/ConnectWallet/ConnectionCenter.tsx +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -1,6 +1,7 @@ import { Popover, ButtonBase, Typography, Paper } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandLessIcon from '@mui/icons-material/ExpandLess' +import classnames from 'classnames' import { useState, type MouseEvent, type ReactElement } from 'react' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' @@ -29,7 +30,6 @@ const ConnectionCenter = (): ReactElement => { Not connected -
    palette.error.main }}> Connect wallet @@ -52,7 +52,7 @@ const ConnectionCenter = (): ReactElement => { }} sx={{ mt: 1 }} > - + diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index b65796b178..5f0442f50a 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,4 +1,4 @@ -import { Divider, Typography } from '@mui/material' +import { Box, Divider, SvgIcon, Typography } from '@mui/material' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' @@ -8,7 +8,11 @@ import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { return ( <> - + + + + + diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index a4b65433e6..b41bbc8399 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -21,6 +21,10 @@ border: 1px solid var(--color-border-light); } +.largeGap { + gap: var(--space-2); +} + .addressName { text-align: center; overflow: hidden; diff --git a/src/components/common/icons/KeyholeIcon/index.tsx b/src/components/common/icons/KeyholeIcon/index.tsx index 013f7a9fb6..98c76e3762 100644 --- a/src/components/common/icons/KeyholeIcon/index.tsx +++ b/src/components/common/icons/KeyholeIcon/index.tsx @@ -1,9 +1,30 @@ -import Keyhole from '@/components/common/icons/KeyholeIcon/keyhole.svg' +import css from '@/components/common/icons/CircularIcon/styles.module.css' +import LockIcon from '@/public/images/common/lock.svg' +import { Badge, SvgIcon } from '@mui/material' -import CircularIcon from '../CircularIcon' - -const KeyholeIcon = ({ size = 40 }: { size?: number }) => { - return +const KeyholeIcon = ({ size = 28 }: { size?: number }) => { + return ( + + + + ) } export default KeyholeIcon diff --git a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx index b0397da9d0..6861af0638 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx @@ -8,6 +8,7 @@ import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' const mockChainInfo = { chainId: '100', + chainName: 'Gnosis Chain', l2: false, nativeCurrency: { symbol: 'ETH', diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index a36e592d26..40225cdf2f 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -78,7 +78,7 @@ export const NetworkFee = ({ height={16} style={{ margin: '-3px 0px -3px 4px' }} />{' '} - Gnosis Chain + {chain?.chainName}
    ) diff --git a/src/components/sidebar/SafeList/index.tsx b/src/components/sidebar/SafeList/index.tsx index c004462395..734ae00339 100644 --- a/src/components/sidebar/SafeList/index.tsx +++ b/src/components/sidebar/SafeList/index.tsx @@ -148,7 +148,7 @@ const SafeList = ({ closeDrawer }: { closeDrawer?: () => void }): ReactElement = ) : ( - + From 8e2d3246f23371f457d72c6f5bfc376bb3731371 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 7 Nov 2023 14:06:20 +0000 Subject: [PATCH 45/52] fix: merge serviceworkers for seedless onboarding (#2750) Side effect: The redirect URI is changed from /serviceworker/redirect to /redirect --- next.config.mjs | 2 +- src/hooks/wallets/mpc/useMPC.ts | 2 +- src/service-workers/index.ts | 3 + .../service-workers/mpc-core-kit-sw.ts | 59 +++++++++++-------- 4 files changed, 38 insertions(+), 28 deletions(-) rename public/serviceworker/sw.js => src/service-workers/mpc-core-kit-sw.ts (93%) diff --git a/next.config.mjs b/next.config.mjs index 1470c8d524..07d1f2a35d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,7 +32,7 @@ const nextConfig = { dirs: ['src'], }, experimental: { - optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns'] + optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns'], }, webpack(config) { config.module.rules.push({ diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index ef580a0f14..94663b4391 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -44,7 +44,7 @@ export const useInitMPC = () => { web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, // Available networks are "sapphire_devnet", "sapphire_mainnet" web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, - baseUrl: `${window.location.origin}/serviceworker`, + baseUrl: `${window.location.origin}/`, uxMode: 'popup', enableLogging: true, chainConfig, diff --git a/src/service-workers/index.ts b/src/service-workers/index.ts index 0ed829a135..70f1e849e8 100644 --- a/src/service-workers/index.ts +++ b/src/service-workers/index.ts @@ -3,5 +3,8 @@ /// import { firebaseMessagingSw } from './firebase-messaging/firebase-messaging-sw' +import { mpcCoreKitServiceWorker } from './mpc-core-kit-sw' firebaseMessagingSw() + +mpcCoreKitServiceWorker() diff --git a/public/serviceworker/sw.js b/src/service-workers/mpc-core-kit-sw.ts similarity index 93% rename from public/serviceworker/sw.js rename to src/service-workers/mpc-core-kit-sw.ts index ad32a6ed6d..76b47bf211 100644 --- a/public/serviceworker/sw.js +++ b/src/service-workers/mpc-core-kit-sw.ts @@ -1,24 +1,30 @@ -/* eslint-disable */ -function getScope() { - return self.registration.scope -} +/// + +declare const self: ServiceWorkerGlobalScope -self.addEventListener('message', function (event) { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting() +export const mpcCoreKitServiceWorker = () => { + /* eslint-disable */ + function getScope() { + return self.registration.scope } -}) -self.addEventListener('fetch', function (event) { - try { - const url = new URL(event.request.url) - const redirectURL = self.location.url - if (url.pathname.includes('redirect') && url.href.includes(getScope())) { - event.respondWith( - new Response( - new Blob( - [ - ` + self.addEventListener('message', function (event) { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } + }) + + self.addEventListener('fetch', function (event) { + try { + const url = new URL(event.request.url) + //@ts-expect-error + const redirectURL = self.location.url + if (url.pathname.includes('redirect') && url.href.includes(getScope())) { + event.respondWith( + new Response( + new Blob( + [ + ` @@ -306,13 +312,14 @@ self.addEventListener('fetch', function (event) { ${''} `, - ], - { type: 'text/html' }, + ], + { type: 'text/html' }, + ), ), - ), - ) + ) + } + } catch (error) { + console.error(error) } - } catch (error) { - console.error(error) - } -}) + }) +} From ea3763b2995942d4ab915c99a1b692f1055349ea Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:56:14 +0100 Subject: [PATCH 46/52] fix: Add feature flag for social login (#2770) * fix: Add feature flag for social login * fix: Show simple Connect button in case feature is disabled * fix: Remove redirects on the welcome page --- .../common/ConnectWallet/ConnectionCenter.tsx | 22 +++++++-- .../__tests__/ConnectionCenter.test.tsx | 18 +++++++ .../common/SocialSigner/PasswordRecovery.tsx | 2 +- .../__tests__/SocialSignerLogin.test.tsx | 48 ++++++++++++++----- src/components/common/SocialSigner/index.tsx | 10 +--- .../welcome/WelcomeLogin/WalletLogin.tsx | 17 ++++--- .../__tests__/WalletLogin.test.tsx | 6 +-- src/components/welcome/WelcomeLogin/index.tsx | 19 +++++--- src/hooks/wallets/wallets.ts | 2 +- src/services/mpc/SocialLoginModule.ts | 6 ++- src/services/mpc/__tests__/module.test.ts | 13 +++-- src/utils/chains.ts | 1 + 12 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx index 8201241a16..1e04b377d2 100644 --- a/src/components/common/ConnectWallet/ConnectionCenter.tsx +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -1,4 +1,8 @@ -import { Popover, ButtonBase, Typography, Paper } from '@mui/material' +import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import madProps from '@/utils/mad-props' +import { Popover, ButtonBase, Typography, Paper, Box } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import classnames from 'classnames' @@ -9,7 +13,7 @@ import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' import css from '@/components/common/ConnectWallet/styles.module.css' -const ConnectionCenter = (): ReactElement => { +export const ConnectionCenter = ({ isSocialLoginEnabled }: { isSocialLoginEnabled: boolean }): ReactElement => { const [anchorEl, setAnchorEl] = useState(null) const open = !!anchorEl @@ -23,6 +27,14 @@ const ConnectionCenter = (): ReactElement => { const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon + if (!isSocialLoginEnabled) { + return ( + + + + ) + } + return ( <> @@ -60,4 +72,8 @@ const ConnectionCenter = (): ReactElement => { ) } -export default ConnectionCenter +const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN) + +export default madProps(ConnectionCenter, { + isSocialLoginEnabled: useIsSocialLoginEnabled, +}) diff --git a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx new file mode 100644 index 0000000000..5ca16f4512 --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx @@ -0,0 +1,18 @@ +import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter' +import { render } from '@/tests/test-utils' + +describe('ConnectionCenter', () => { + it('displays a Connect wallet button if the social login feature is enabled', () => { + const { getByText, queryByText } = render() + + expect(getByText('Connect wallet')).toBeInTheDocument() + expect(queryByText('Connect')).not.toBeInTheDocument() + }) + + it('displays the ConnectWalletButton if the social login feature is disabled', () => { + const { getByText, queryByText } = render() + + expect(queryByText('Connect wallet')).not.toBeInTheDocument() + expect(getByText('Connect')).toBeInTheDocument() + }) +}) diff --git a/src/components/common/SocialSigner/PasswordRecovery.tsx b/src/components/common/SocialSigner/PasswordRecovery.tsx index 5a40e95756..084a949061 100644 --- a/src/components/common/SocialSigner/PasswordRecovery.tsx +++ b/src/components/common/SocialSigner/PasswordRecovery.tsx @@ -27,7 +27,7 @@ export const PasswordRecovery = ({ onSuccess, }: { recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise - onSuccess: (() => void) | undefined + onSuccess?: (() => void) | undefined }) => { const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx index af20b8e3c3..3086a6e096 100644 --- a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx +++ b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx @@ -2,7 +2,7 @@ import { act, render, waitFor } from '@/tests/test-utils' import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS, type UserInfo, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import SocialWalletService from '@/services/mpc/SocialWalletService' import { TxModalProvider } from '@/components/tx-flow' import { fireEvent } from '@testing-library/react' @@ -51,7 +51,7 @@ describe('SocialSignerLogin', () => { expect(mockOnLogin).toHaveBeenCalled() }) - it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => { + it('should render google login button if no wallet is connected on gnosis chain', async () => { const mockOnLogin = jest.fn() const result = render( @@ -70,18 +70,30 @@ describe('SocialSignerLogin', () => { expect(result.findByText('Continue with Google')).resolves.toBeDefined() expect(await result.findByRole('button')).toBeEnabled() }) + }) - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() + it('should display a Continue as button and call onLogin when clicked', () => { + const mockOnLogin = jest.fn() + mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.LOGGED_IN)) - const button = await result.findByRole('button') - act(() => { - button.click() - }) + const result = render( + + + , + ) - await waitFor(async () => { - expect(mockOnLogin).toHaveBeenCalled() - }) + expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() + + const button = result.getByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() }) it('should disable the Google Login button with a message when not on gnosis chain', async () => { @@ -98,10 +110,11 @@ describe('SocialSignerLogin', () => { expect(await result.findByRole('button')).toBeDisabled() }) - it('should display Password Recovery form and call onLogin if password recovery succeeds', async () => { + it('should display Password Recovery form and display a Continue as button when login succeeds', async () => { const mockOnLogin = jest.fn() mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE)) mockSocialWalletService.getUserInfo = jest.fn(undefined) + mockSocialWalletService.recoverAccountWithPassword = jest.fn(() => Promise.resolve(true)) const result = render( @@ -140,8 +153,17 @@ describe('SocialSignerLogin', () => { submitButton.click() }) + mockSocialWalletService.getUserInfo = jest.fn( + () => + ({ + email: 'test@testermann.com', + name: 'Test Testermann', + profileImage: 'test.testermann.local/profile.png', + } as unknown as UserInfo), + ) + await waitFor(() => { - expect(mockOnLogin).toHaveBeenCalled() + expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() }) }) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index c70f2c76bf..503327fd49 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -70,11 +70,10 @@ export const SocialSigner = ({ const success = await socialWalletService.recoverAccountWithPassword(password, storeDeviceFactor) if (success) { - onLogin?.() setTxFlow(undefined) } }, - [onLogin, setTxFlow, socialWalletService], + [setTxFlow, socialWalletService], ) const login = async () => { @@ -86,7 +85,6 @@ export const SocialSigner = ({ const status = await socialWalletService.loginAndCreate() if (status === COREKIT_STATUS.LOGGED_IN) { - onLogin?.() setLoginPending(false) return } @@ -94,11 +92,7 @@ export const SocialSigner = ({ if (status === COREKIT_STATUS.REQUIRED_SHARE) { onRequirePassword?.() - setTxFlow( - , - () => setLoginPending(false), - false, - ) + setTxFlow(, () => setLoginPending(false), false) return } } catch (err) { diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 21674d16a8..c89c4f1f8e 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -10,14 +10,6 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const wallet = useWallet() const connectWallet = useConnectWallet() - const login = async () => { - const walletState = await connectWallet() - - if (walletState && walletState.length > 0) { - onLogin() - } - } - const isSocialLogin = isSocialLoginWallet(wallet?.label) if (wallet !== null && !isSocialLogin) { @@ -55,7 +47,14 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { } return ( - ) diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx index 4a4feb1ea9..53e057b7c4 100644 --- a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -37,7 +37,7 @@ describe('WalletLogin', () => { expect(mockOnLogin).toHaveBeenCalled() }) - it('should render connect wallet and invoke the callback on connection if no wallet is connected', async () => { + it('should render connect wallet if no wallet is connected', async () => { const mockOnLogin = jest.fn() const walletAddress = hexZeroPad('0x1', 20) const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) @@ -49,7 +49,7 @@ describe('WalletLogin', () => { expect(result.findByText('Connect wallet')).resolves.toBeDefined() }) - // We do not automatically invoke the callback as the user did not actively connect + // We do not automatically invoke the callback expect(mockOnLogin).not.toHaveBeenCalled() await act(async () => { @@ -65,7 +65,7 @@ describe('WalletLogin', () => { }) await waitFor(() => { - expect(mockOnLogin).toHaveBeenCalled() + expect(result.getByText('Connect wallet')).toBeInTheDocument() }) }) diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index a80237343a..ad768bf84a 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,5 +1,7 @@ import SocialSigner from '@/components/common/SocialSigner' import { AppRoutes } from '@/config/routes' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' import SafeLogo from '@/public/images/logo-text.svg' import css from './styles.module.css' @@ -11,6 +13,7 @@ import { trackEvent } from '@/services/analytics' const WelcomeLogin = () => { const router = useRouter() + const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN) const continueToCreation = () => { trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION) @@ -29,13 +32,17 @@ const WelcomeLogin = () => { - - - or - - + {isSocialLoginEnabled && ( + <> + + + or + + - + + + )} Already have a Safe Account? diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index c55c23f9c2..2de9efdd29 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -43,7 +43,7 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain), [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }), [WALLET_KEYS.PAIRING]: () => pairingModule(), - [WALLET_KEYS.SOCIAL]: () => MpcModule(), + [WALLET_KEYS.SOCIAL]: (chain) => MpcModule(chain), [WALLET_KEYS.LEDGER]: () => ledgerModule(), [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }), [WALLET_KEYS.KEYSTONE]: () => keystoneModule(), diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts index aeafa92bde..6e34ce89d8 100644 --- a/src/services/mpc/SocialLoginModule.ts +++ b/src/services/mpc/SocialLoginModule.ts @@ -2,6 +2,8 @@ import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' import { getSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' import * as PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' +import { FEATURES, hasFeature } from '@/utils/chains' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' import { type EIP1193Provider } from '@web3-onboard/core' import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' @@ -40,7 +42,9 @@ const getConnectedAccounts = async () => { * * @returns Custom Onboard MpcModule */ -function MpcModule(): WalletInit { +function MpcModule(chain: ChainInfo): WalletInit { + if (!hasFeature(chain, FEATURES.SOCIAL_LOGIN)) return () => null + return () => { return { label: ONBOARD_MPC_MODULE_LABEL, diff --git a/src/services/mpc/__tests__/module.test.ts b/src/services/mpc/__tests__/module.test.ts index 350bdf65bb..f90a2021c3 100644 --- a/src/services/mpc/__tests__/module.test.ts +++ b/src/services/mpc/__tests__/module.test.ts @@ -1,3 +1,5 @@ +import { chainBuilder } from '@/tests/builders/chains' +import { FEATURES } from '@/utils/chains' import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../SocialLoginModule' import { type WalletModule } from '@web3-onboard/common' @@ -5,9 +7,14 @@ import * as web3 from '@/hooks/wallets/web3' import * as useMPC from '@/hooks/wallets/mpc/useMPC' import { hexZeroPad } from 'ethers/lib/utils' +const mockChain = chainBuilder() + // @ts-expect-error - we are using a local FEATURES enum + .with({ features: [FEATURES.SOCIAL_LOGIN] }) + .build() + describe('MPC Onboard module', () => { it('should return correct metadata', async () => { - const mpcModule = MpcModule()({ + const mpcModule = MpcModule(mockChain)({ device: { browser: { name: 'Firefox', @@ -42,7 +49,7 @@ describe('MPC Onboard module', () => { } as any }) - const mpcModule = MpcModule()({ + const mpcModule = MpcModule(mockChain)({ device: { browser: { name: 'Firefox', @@ -93,7 +100,7 @@ describe('MPC Onboard module', () => { } as any }) - const mpcModule = MpcModule()({ + const mpcModule = MpcModule(mockChain)({ device: { browser: { name: 'Firefox', diff --git a/src/utils/chains.ts b/src/utils/chains.ts index f4bcf886fb..a5e1eaf128 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -16,6 +16,7 @@ export enum FEATURES { RISK_MITIGATION = 'RISK_MITIGATION', PUSH_NOTIFICATIONS = 'PUSH_NOTIFICATIONS', NATIVE_WALLETCONNECT = 'NATIVE_WALLETCONNECT', + SOCIAL_LOGIN = 'SOCIAL_LOGIN', } export const hasFeature = (chain: ChainInfo, feature: FEATURES): boolean => { From a299a3313938b4ff2a6faf49b6eef20f3c7d5735 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 8 Nov 2023 15:18:29 +0100 Subject: [PATCH 47/52] chore: update mpc-core-kit (#2769) * chore: update mpc-core-kit * feat: set constant hashedFactorNonce --- package.json | 2 +- src/hooks/wallets/mpc/useMPC.ts | 1 + yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fd5ac3cfeb..f2cabbfaf3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.7", - "@web3auth/mpc-core-kit": "^1.1.2", + "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", "bn.js": "^5.2.1", "classnames": "^2.3.1", diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index 94663b4391..2fc22fe65e 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -49,6 +49,7 @@ export const useInitMPC = () => { enableLogging: true, chainConfig, manualSync: true, + hashedFactorNonce: 'safe-global-sfa-nonce', }) web3AuthCoreKit diff --git a/yarn.lock b/yarn.lock index 7992a4b078..04aa30539d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6195,10 +6195,10 @@ loglevel "^1.8.1" ts-custom-error "^3.3.1" -"@web3auth/mpc-core-kit@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.1.2.tgz#308f9d441b1275ebcc2c96be8ff976decee6dbcf" - integrity sha512-bx16zYdC3D2KPp5wv55fn6W3RcMGUUbHeoClaDI2czwbUrZyql71A4qQdyi6tMTzy/uAXWZrfB+U4NGk+ec9Pw== +"@web3auth/mpc-core-kit@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@web3auth/mpc-core-kit/-/mpc-core-kit-1.1.3.tgz#01c508157f11d5a6685fa578bb873f40678fa403" + integrity sha512-wpbrBDBxZ8vi7oY3zL3BEktKuwfs5miMga4R7qmc12k79gNJa8S6FvZQ38wZkwNJyrRb0iFQ1kOWR0NOteeMyg== dependencies: "@tkey-mpc/chrome-storage" "^9.0.2" "@tkey-mpc/common-types" "^9.0.2" From 4fbfa1b7da76cd2671a3ad0b939b8a4327d3cb76 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:18:47 +0100 Subject: [PATCH 48/52] fix: Add events for export pk (#2766) --- .../ExportMPCAccountModal.tsx | 24 ++++++++++----- .../SocialSignerExport/index.tsx | 29 ++++++++++++------- src/services/analytics/events/mpcWallet.ts | 25 ++++++++++++++++ 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx index ffb7cb461d..bacce6a38f 100644 --- a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx @@ -1,5 +1,7 @@ import CopyButton from '@/components/common/CopyButton' import ModalDialog from '@/components/common/ModalDialog' +import { trackEvent } from '@/services/analytics' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typography } from '@mui/material' import { useState } from 'react' import { useForm } from 'react-hook-form' @@ -43,9 +45,11 @@ const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: b try { setError(undefined) const pk = await socialWalletService.exportSignerKey(data[ExportFieldNames.password]) + trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_SUCCESS) setValue(ExportFieldNames.pk, pk) } catch (err) { logError(ErrorCodes._305, err) + trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_ERROR) setError(asError(err).message) } } @@ -55,13 +59,19 @@ const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: b reset() onClose() } + + const toggleShowPK = () => { + trackEvent(MPC_WALLET_EVENTS.SEE_PK) + setShowPassword((prev) => !prev) + } + + const onCopy = () => { + trackEvent(MPC_WALLET_EVENTS.COPY_PK) + } + return ( - - - Export your account - - + Export your account @@ -83,10 +93,10 @@ const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: b readOnly: true, endAdornment: ( <> - setShowPassword((prev) => !prev)}> + {showPassword ? : } - + ), }} diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx index c73a0b7af1..088e484af2 100644 --- a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx @@ -1,3 +1,5 @@ +import Track from '@/components/common/Track' +import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' import { Alert, Box, Button, Tooltip, Typography } from '@mui/material' import { useState } from 'react' import ExportMPCAccountModal from '@/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal' @@ -20,17 +22,22 @@ const SocialSignerExport = () => { Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over your social login signer. - - - + + + + + + + + setIsModalOpen(false)} open={isModalOpen} /> diff --git a/src/services/analytics/events/mpcWallet.ts b/src/services/analytics/events/mpcWallet.ts index 5f3277c789..8e20e29de2 100644 --- a/src/services/analytics/events/mpcWallet.ts +++ b/src/services/analytics/events/mpcWallet.ts @@ -28,4 +28,29 @@ export const MPC_WALLET_EVENTS = { action: 'Enable MFA for account', category: MPC_WALLET_CATEGORY, }, + REVEAL_PRIVATE_KEY: { + event: EventType.CLICK, + action: 'Reveal private key', + category: MPC_WALLET_CATEGORY, + }, + EXPORT_PK_SUCCESS: { + event: EventType.META, + action: 'Export private key successful', + category: MPC_WALLET_CATEGORY, + }, + EXPORT_PK_ERROR: { + event: EventType.META, + action: 'Export private key error', + category: MPC_WALLET_CATEGORY, + }, + SEE_PK: { + event: EventType.CLICK, + action: 'Toggle see private key', + category: MPC_WALLET_CATEGORY, + }, + COPY_PK: { + event: EventType.CLICK, + action: 'Copy private key', + category: MPC_WALLET_CATEGORY, + }, } From c3e0cb840b63961c2b6ebc183f949c6027dbbd4e Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:28:12 +0100 Subject: [PATCH 49/52] fix: Update welcome screen text (#2773) --- src/components/welcome/NewSafeSocial.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/welcome/NewSafeSocial.tsx b/src/components/welcome/NewSafeSocial.tsx index 26c1d1da77..590e9625f7 100644 --- a/src/components/welcome/NewSafeSocial.tsx +++ b/src/components/welcome/NewSafeSocial.tsx @@ -48,7 +48,7 @@ const NewSafeSocial = () => { letterSpacing={-1.5} color="static.main" > - Get started in 30 seconds with your Google Account + Get the most secure web3 account in {'<'}30 seconds
      @@ -57,8 +57,7 @@ const NewSafeSocial = () => {
    - {/* TODO: Replace with actual link and possibly add tracking */} - + From 86800bd233be7a51c6f59b4e78475776cd965072 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 8 Nov 2023 17:05:42 +0100 Subject: [PATCH 50/52] fix: disable logs in prod (#2774) --- src/hooks/wallets/mpc/useMPC.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index 2fc22fe65e..b0b9d8544a 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -9,6 +9,7 @@ import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/u import { useInitSocialWallet } from './useSocialWallet' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' +import { IS_PRODUCTION } from '@/config/constants' const { getStore, setStore, useStore } = new ExternalStore() @@ -46,7 +47,7 @@ export const useInitMPC = () => { web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, baseUrl: `${window.location.origin}/`, uxMode: 'popup', - enableLogging: true, + enableLogging: !IS_PRODUCTION, chainConfig, manualSync: true, hashedFactorNonce: 'safe-global-sfa-nonce', From 7536b5a3c8e211b4b245ac7255d9c2a2656a011c Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 8 Nov 2023 17:09:35 +0100 Subject: [PATCH 51/52] chore: Bump version to v1.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2cabbfaf3..26ac24be3c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", "type": "module", - "version": "1.21.0", + "version": "1.22.0", "scripts": { "dev": "next dev", "start": "next dev", From 73c5c732ebe4cbf3d61657a8ca05b1c5fd787f63 Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 9 Nov 2023 14:57:46 +0100 Subject: [PATCH 52/52] fix: link to welcome page in wc --- src/pages/wc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/wc.tsx b/src/pages/wc.tsx index 02c41159db..69dcb26b79 100644 --- a/src/pages/wc.tsx +++ b/src/pages/wc.tsx @@ -26,7 +26,7 @@ const WcPage: NextPage = () => { }, } : { - pathname: AppRoutes.welcome, + pathname: AppRoutes.welcome.index, query: { [WC_URI_SEARCH_PARAM]: uri, },