From f31099bafce7fed7029941a8e78729b55c9365dd Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 23 Oct 2023 11:32:50 +0200 Subject: [PATCH 1/2] [Seedless-Onboarding] Add signer account to address book on login (#2672) --- .../mpc/__tests__/useMPCWallet.test.ts | 23 +++++++++++++++++-- src/hooks/wallets/mpc/useMPCWallet.ts | 22 +++++++++++++++++- src/utils/addresses.ts | 6 ++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts index 53a0b60985..176e7b777f 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts @@ -14,9 +14,15 @@ 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 @@ -67,6 +73,10 @@ class MockMPCCoreKit { commitChanges() { return Promise.resolve() } + + getUserInfo() { + return this.state.userInfo + } } describe('useMPCWallet', () => { @@ -76,6 +86,7 @@ describe('useMPCWallet', () => { beforeEach(() => { jest.resetAllMocks() setMPCCoreKitInstance(undefined) + jest.spyOn(useChainId, 'default').mockReturnValue('100') }) afterAll(() => { jest.useRealTimers() @@ -94,6 +105,7 @@ describe('useMPCWallet', () => { }) 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()) @@ -102,8 +114,10 @@ describe('useMPCWallet', () => { }) 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().mockImplementation(() => Promise.resolve()) + const connectWalletSpy = jest.fn().mockResolvedValue([{ accounts: [{ address: mockSignerAddress }] }]) jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) setMPCCoreKitInstance( new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, { @@ -137,13 +151,17 @@ describe('useMPCWallet', () => { 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().mockImplementation(() => Promise.resolve()) + const connectWalletSpy = jest.fn().mockResolvedValue([{ accounts: [{ address: mockSignerAddress }] }]) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) setMPCCoreKitInstance( new MockMPCCoreKit( @@ -184,6 +202,7 @@ describe('useMPCWallet', () => { disableModals: true, }, }) + expect(upsertABSpy).not.toHaveBeenCalled() }) }) diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index 06328e4e82..1c7c1416af 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -9,6 +9,11 @@ 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, @@ -32,6 +37,9 @@ 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 @@ -101,12 +109,24 @@ export const useMPCWallet = (): MPCWalletHook => { if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { await mpcCoreKit.commitChanges() - await connectWallet(onboard, { + 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) } } diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index e1c4bfce68..8718efbd22 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -1,6 +1,10 @@ import { getAddress } from 'ethers/lib/utils' import { isAddress } from '@ethersproject/address' - +/** + * Checksums the given address + * @param address ethereum address + * @returns the checksummed address if the given address is valid otherwise returns the address unchanged + */ export const checksumAddress = (address: string): string => { return isAddress(address) ? getAddress(address) : address } From 5d05596b4d3bc730aba3211e4e6316ade51f60bd Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:10:45 +0200 Subject: [PATCH 2/2] fix: Address design review, restructure components (#2683) --- .../common/ConnectWallet/PasswordRecovery.tsx | 2 +- .../ExportMPCAccountModal.tsx | 2 +- .../SocialSignerExport}/index.tsx | 10 ++-- .../SocialSignerExport}/styles.module.css | 0 .../SocialSignerMFA}/PasswordInput.tsx | 0 .../SocialSignerMFA}/helper.ts | 0 .../SocialSignerMFA/index.test.tsx} | 2 +- .../SocialSignerMFA/index.tsx} | 30 ++++++----- .../SocialSignerMFA}/styles.module.css | 0 .../settings/SecurityLogin/index.tsx | 51 +++++++++++++++++++ .../settings/SignerAccountMFA/index.tsx | 23 --------- .../sidebar/SidebarNavigation/config.tsx | 8 +-- .../welcome/WelcomeLogin/WalletLogin.tsx | 2 +- .../__tests__/WalletLogin.test.tsx | 2 - src/config/routes.ts | 4 +- src/pages/settings/security-login.tsx | 23 +++++++++ src/pages/settings/signer-account.tsx | 50 ------------------ 17 files changed, 106 insertions(+), 103 deletions(-) rename src/components/settings/{ExportMPCAccount => SecurityLogin/SocialSignerExport}/ExportMPCAccountModal.tsx (98%) rename src/components/settings/{ExportMPCAccount => SecurityLogin/SocialSignerExport}/index.tsx (72%) rename src/components/settings/{ExportMPCAccount => SecurityLogin/SocialSignerExport}/styles.module.css (100%) rename src/components/settings/{SignerAccountMFA => SecurityLogin/SocialSignerMFA}/PasswordInput.tsx (100%) rename src/components/settings/{SignerAccountMFA => SecurityLogin/SocialSignerMFA}/helper.ts (100%) rename src/components/settings/{SignerAccountMFA/PasswordForm.test.tsx => SecurityLogin/SocialSignerMFA/index.test.tsx} (96%) rename src/components/settings/{SignerAccountMFA/PasswordForm.tsx => SecurityLogin/SocialSignerMFA/index.tsx} (89%) rename src/components/settings/{SignerAccountMFA => SecurityLogin/SocialSignerMFA}/styles.module.css (100%) create mode 100644 src/components/settings/SecurityLogin/index.tsx delete mode 100644 src/components/settings/SignerAccountMFA/index.tsx create mode 100644 src/pages/settings/security-login.tsx delete mode 100644 src/pages/settings/signer-account.tsx diff --git a/src/components/common/ConnectWallet/PasswordRecovery.tsx b/src/components/common/ConnectWallet/PasswordRecovery.tsx index 053a533a27..d5c47fff6c 100644 --- a/src/components/common/ConnectWallet/PasswordRecovery.tsx +++ b/src/components/common/ConnectWallet/PasswordRecovery.tsx @@ -13,7 +13,7 @@ import { import { useState } from 'react' import Track from '../Track' import { FormProvider, useForm } from 'react-hook-form' -import PasswordInput from '@/components/settings/SignerAccountMFA/PasswordInput' +import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' type PasswordFormData = { password: string diff --git a/src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx similarity index 98% rename from src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx rename to src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx index efffab0b71..b51a9c7282 100644 --- a/src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx @@ -5,7 +5,7 @@ import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typogra 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 css from '@/components/settings/SecurityLogin/SocialSignerExport/styles.module.css' import ErrorCodes from '@/services/exceptions/ErrorCodes' import { logError } from '@/services/exceptions' import ErrorMessage from '@/components/tx/ErrorMessage' diff --git a/src/components/settings/ExportMPCAccount/index.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx similarity index 72% rename from src/components/settings/ExportMPCAccount/index.tsx rename to src/components/settings/SecurityLogin/SocialSignerExport/index.tsx index d8c2b43601..46900f325a 100644 --- a/src/components/settings/ExportMPCAccount/index.tsx +++ b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx @@ -1,19 +1,19 @@ import { Alert, Box, Button, Typography } from '@mui/material' import { useState } from 'react' -import ExportMPCAccountModal from './ExportMPCAccountModal' +import ExportMPCAccountModal from '@/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal' -const ExportMPCAccount = () => { +const SocialSignerExport = () => { const [isModalOpen, setIsModalOpen] = useState(false) return ( <> - Accounts created via Google can be exported and imported to any non-custodial wallet outside of Safe. + Signers 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. + your social login signer.