Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Seedless-Onboarding] export account #2610

Merged
merged 13 commits into from
Oct 20, 2023
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/components/common/ConnectWallet/MPCWalletProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const MpcWalletContext = createContext<MPCWalletHook>({
upsertPasswordBackup: () => Promise.resolve(),
recoverFactorWithPassword: () => Promise.resolve(false),
userInfo: undefined,
exportPk: () => Promise.resolve(undefined),
})

export const MpcWalletProvider = ({ children }: { children: ReactElement }) => {
Expand Down
127 changes: 127 additions & 0 deletions src/components/settings/ExportMPCAccount/ExportMPCAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -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<string>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Just found out we can even use RHF to display an error separate from its input https://react-hook-form.com/docs/useformstate/errormessage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although on second thought if we set the error via RHF it will not be persisted afair so this is probably better.


const [showPassword, setShowPassword] = useState(false)
const formMethods = useForm<ExportFormData>({
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 (
<ModalDialog open={open} onClose={handleClose}>
<DialogTitle>
<Typography variant="h6" fontWeight={700}>
Export your account
</Typography>
</DialogTitle>
<IconButton className={css.close} aria-label="close" onClick={handleClose} size="small">
<Close fontSize="large" />
</IconButton>

<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<Box display="flex" flexDirection="column" gap={2} alignItems="flex-start" sx={{ width: '100%' }}>
<Typography>For security reasons you have to enter your password to reveal your account key.</Typography>

{exportedKey ? (
<Box display="flex" flexDirection="row" alignItems="center" gap={1} width="100%">
<TextField
fullWidth
multiline={showPassword}
maxRows={3}
label="Private key"
type="password"
InputProps={{
readOnly: true,
endAdornment: (
<>
<IconButton size="small" onClick={() => setShowPassword((prev) => !prev)}>
{showPassword ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
</IconButton>
<CopyButton text={exportedKey} />
</>
),
}}
{...register(ExportFieldNames.pk)}
/>
</Box>
) : (
<>
<TextField
placeholder="Password"
label="Password"
type="password"
fullWidth
error={!!formState.errors[ExportFieldNames.password]}
helperText={formState.errors[ExportFieldNames.password]?.message}
{...register(ExportFieldNames.password, {
required: true,
})}
/>
</>
)}
{error && <ErrorMessage className={css.modalError}>{error}</ErrorMessage>}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ErrorMessage component adds vertical margin by default which makes the UI jump a bit more. I would suggest removing it via the css class.


<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center" width="100%">
<Button variant="outlined" onClick={handleClose}>
Close
</Button>
{exportedKey === undefined && (
<Button color="primary" variant="contained" disabled={formState.isSubmitting} type="submit">
Export
</Button>
)}
</Box>
</Box>
</form>
</DialogContent>
</ModalDialog>
)
}

export default ExportMPCAccountModal
27 changes: 27 additions & 0 deletions src/components/settings/ExportMPCAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Box display="flex" flexDirection="column" gap={2} alignItems="flex-start">
<Typography>
Accounts created via Google can be exported and imported to any non-custodial wallet outside of Safe.
</Typography>
<Alert severity="warning">
Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over
your signer account.
</Alert>
<Button color="primary" variant="contained" disabled={isModalOpen} onClick={() => setIsModalOpen(true)}>
Reveal private key
</Button>
</Box>
<ExportMPCAccountModal onClose={() => setIsModalOpen(false)} open={isModalOpen} />
</>
)
}

export default ExportMPCAccount
10 changes: 10 additions & 0 deletions src/components/settings/ExportMPCAccount/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.close {
position: absolute;
right: var(--space-1);
top: var(--space-1);
}

.modalError {
width: 100%;
margin: 0;
}
20 changes: 20 additions & 0 deletions src/hooks/wallets/mpc/useMPCWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type MPCWalletHook = {
triggerLogin: () => Promise<boolean>
resetAccount: () => Promise<void>
userInfo: UserInfo | undefined
exportPk: (password: string) => Promise<string | undefined>
}

export const useMPCWallet = (): MPCWalletHook => {
Expand Down Expand Up @@ -132,12 +133,31 @@ export const useMPCWallet = (): MPCWalletHook => {
return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN
}

const exportPk = async (password: string): Promise<string> => {
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,
recoverFactorWithPassword,
resetAccount: criticalResetAccount,
upsertPasswordBackup: () => Promise.resolve(),
userInfo: mpcCoreKit?.state.userInfo,
exportPk,
}
}
13 changes: 13 additions & 0 deletions src/pages/settings/signer-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -29,6 +30,18 @@ const SignerAccountPage: NextPage = () => {
</Grid>
</Grid>
</Paper>
<Paper sx={{ p: 4, mt: 2 }}>
<Grid container spacing={3}>
<Grid item lg={4} xs={12}>
<Typography variant="h4" fontWeight="bold" mb={1}>
Account export
</Typography>
</Grid>
<Grid item xs>
<ExportMPCAccount />
</Grid>
</Grid>
</Paper>
</main>
</>
)
Expand Down
1 change: 1 addition & 0 deletions src/services/exceptions/ErrorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down