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==