Implement functionality to import wallet from mnemonic (#13)

Part of cerc-io/laconic-wallet-web#11

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet-web#13
This commit is contained in:
nabarun 2024-08-09 10:24:38 +00:00
parent c26bddec1a
commit 2bb92205ba
6 changed files with 139 additions and 20 deletions

View File

@ -1,5 +1,5 @@
import { View } from 'react-native';
import React from 'react';
import { View } from 'react-native';
import { Button } from 'react-native-paper';
import { CreateWalletProps } from '../types';

View File

@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, Grid, Button, Typography } from "@mui/material";
const ImportWalletDialog = ({
visible,
hideDialog,
importWalletHandler
}: {
visible: boolean;
hideDialog: () => void;
importWalletHandler: (recoveryPhrase: string) => Promise<void>;
}) => {
const [words, setWords] = useState(Array(12).fill(''));
const handleWordChange = (index: number, value: string) => {
const newWords = [...words];
newWords[index] = value;
setWords(newWords);
};
const handlePaste = (event: React.ClipboardEvent<HTMLDivElement>) => {
const pastedText = event.clipboardData.getData('Text');
const splitWords = pastedText.trim().split(/\s+/);
if (splitWords.length === 12) {
setWords(splitWords);
event.preventDefault();
}
};
useEffect(() => {
setWords(Array(12).fill(''));
},[visible]);
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle>Import your wallet from your mnemonic</DialogTitle>
<DialogContent>
<Typography>
(You can paste your entire mnemonic into the first textbox)
</Typography>
<Grid container spacing={2}>
{words.map((word, index) => (
<Grid item xs={6} sm={4} key={index}>
<TextField
value={word}
onChange={(e) => handleWordChange(index, e.target.value)}
onPaste={index === 0 ? handlePaste : undefined}
placeholder={`Word ${index + 1}`}
fullWidth
margin="normal"
/>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => importWalletHandler(words.join(' '))}>Import Wallet</Button>
<Button onClick={hideDialog}>Cancel</Button>
</DialogActions>
</Dialog>
);
}
export default ImportWalletDialog;

View File

@ -5,7 +5,7 @@ import styles from '../styles/stylesheet';
import GridView from './Grid';
import { CustomDialogProps } from '../types';
const DialogComponent = ({ visible, hideDialog, contentText }: CustomDialogProps) => {
const MnemonicDialog = ({ visible, hideDialog, contentText }: CustomDialogProps) => {
const words = contentText.split(' ');
const copyMnemonic = () => {
@ -26,10 +26,10 @@ const DialogComponent = ({ visible, hideDialog, contentText }: CustomDialogProps
</DialogContent>
<DialogActions>
<Button onClick={copyMnemonic}>Copy</Button>
<Button onClick={hideDialog}>Done</Button>
<Button onClick={hideDialog}>Cancel</Button>
</DialogActions>
</Dialog>
);
};
export { DialogComponent };
export { MnemonicDialog };

View File

@ -5,9 +5,11 @@ import { Button, Text } from 'react-native-paper';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { getSdkError } from '@walletconnect/utils';
import { Portal, Snackbar } from '@mui/material';
import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
import { DialogComponent } from '../components/Dialog';
import { MnemonicDialog } from '../components/MnemonicDialog';
import ImportWalletDialog from '../components/ImportWalletDialog';
import { NetworkDropdown } from '../components/NetworkDropdown';
import Accounts from '../components/Accounts';
import CreateWallet from '../components/CreateWallet';
@ -55,12 +57,15 @@ const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
const [isWalletCreating, setIsWalletCreating] = useState<boolean>(false);
const [walletDialog, setWalletDialog] = useState<boolean>(false);
const [importWalletDialog, setImportWalletDialog] = useState<boolean>(false);
const [mnemonicDialog, setMnemonicDialog] = useState<boolean>(false);
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [toastVisible, setToastVisible] = useState(false);
const [invalidMnemonicError, setInvalidMnemonicError] = useState('');
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(true);
const [phrase, setPhrase] = useState('');
const hideWalletDialog = () => setWalletDialog(false);
const hideMnemonicDialog = () => setMnemonicDialog(false);
const hideResetDialog = () => setResetWalletDialog(false);
const fetchAccounts = useCallback(async () => {
@ -83,12 +88,27 @@ const HomeScreen = () => {
const mnemonic = await createWallet(networksData);
if (mnemonic) {
fetchAccounts();
setWalletDialog(true);
setMnemonicDialog(true);
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
}
};
const importWalletHandler = async (recoveryPhrase: string) => {
try{
const mnemonic = await createWallet(networksData, recoveryPhrase);
if (mnemonic) {
fetchAccounts();
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
setImportWalletDialog(false);
}
} catch (error: any) {
setInvalidMnemonicError((error.message as string).toUpperCase())
setToastVisible(true);
}
};
const confirmResetWallet = useCallback(async () => {
setIsWalletCreated(false);
setIsWalletCreating(false);
@ -152,14 +172,28 @@ const HomeScreen = () => {
</View>
</>
) : (
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
<>
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
<View style={styles.createWalletContainer}>
<Button
mode="contained"
onPress={() => setImportWalletDialog(true)}>
Import Wallet
</Button>
</View>
</>
)}
<DialogComponent
visible={walletDialog}
hideDialog={hideWalletDialog}
<ImportWalletDialog
visible={importWalletDialog}
hideDialog={() => setImportWalletDialog(false)}
importWalletHandler={importWalletHandler}
/>
<MnemonicDialog
visible={mnemonicDialog}
hideDialog={hideMnemonicDialog}
contentText={phrase}
/>
<ConfirmDialog
@ -168,6 +202,16 @@ const HomeScreen = () => {
hideDialog={hideResetDialog}
onConfirm={confirmResetWallet}
/>
<Portal>
<Snackbar
open={toastVisible}
autoHideDuration={3000}
message={invalidMnemonicError}
onClose={() => setToastVisible(false)}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
ContentProps={{ style: { backgroundColor: 'red', color: 'white'} }}
/>
</Portal>
</View>
);
};

View File

@ -5,7 +5,7 @@ const styles = StyleSheet.create({
marginTop: 20,
width: 150,
alignSelf: 'center',
marginBottom: 40
marginBottom: 30,
},
signLink: {
alignItems: 'flex-end',

View File

@ -27,12 +27,23 @@ import { COSMOS, EIP155 } from './constants';
const createWallet = async (
networksData: NetworksDataState[],
recoveryPhrase?: string,
): Promise<string> => {
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
const mnemonic = recoveryPhrase ? recoveryPhrase : utils.entropyToMnemonic(utils.randomBytes(16));
const hdNode = HDNode.fromMnemonic(mnemonic);
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
await createWalletFromMnemonic(networksData, hdNode, mnemonic);
return mnemonic;
};
const createWalletFromMnemonic = async (
networksData: NetworksDataState[],
hdNode: HDNode,
mnemonic: string
): Promise<void> => {
for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
@ -73,8 +84,6 @@ const createWallet = async (
),
]);
}
return mnemonic;
};
const addAccount = async (