Retrieve accounts on reload (#29)

* Display HD path on sign message page

* Create component for displaying account details

* Add retrieve accounts function

* Load accounts after closing app

* Fix the retrieve accounts function

* Use hdpath instead of id

* Check if keystore is empty while retrieving accounts

* Add spinner when accounts are being fetched

* Display complete hd paths after reloading the app

* Remove any return type

* Store public key and address

* Modify sign message function to use path

* Fix the add accounts functionality
This commit is contained in:
IshaVenikar 2024-02-22 11:32:25 +05:30 committed by GitHub
parent 01373697f4
commit 783758be39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 235 additions and 139 deletions

View File

@ -0,0 +1,31 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import { Account } from '../types';
import styles from '../styles/stylesheet';
interface AccountDetailsProps {
account: Account | undefined;
}
const AccountDetails: React.FC<AccountDetailsProps> = ({ account }) => {
return (
<View style={styles.accountContainer}>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Address: </Text>
{account?.address}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Public Key: </Text>
{account?.pubKey}
</Text>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>HD Path: </Text>
{account?.hdPath}
</Text>
</View>
);
};
export default AccountDetails;

View File

@ -1,12 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { TouchableOpacity, View } from 'react-native'; import { ScrollView, TouchableOpacity, View } from 'react-native';
import { Button, List, Text, useTheme } from 'react-native-paper'; import { Button, List, Text, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AccountsProps, StackParamsList, Account } from '../types'; import { AccountsProps, StackParamsList, Account } from '../types';
import { addAccount } from '../utils/Accounts'; import { addAccount } from '../utils/accounts';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import HDPathDialog from './HDPathDialog'; import HDPathDialog from './HDPathDialog';
import AccountDetails from './AccountDetails';
const Accounts = ({ const Accounts = ({
network, network,
@ -59,72 +62,61 @@ const Accounts = ({
)); ));
return ( return (
<View> <ScrollView>
<HDPathDialog <View>
visible={hdDialog} <HDPathDialog
hideDialog={() => setHdDialog(false)} visible={hdDialog}
updateAccounts={updateAccounts} hideDialog={() => setHdDialog(false)}
updateIndex={updateId} updateAccounts={updateAccounts}
pathCode={pathCode} updateIndex={updateId}
/> pathCode={pathCode}
<List.Accordion />
title={`Account ${currentIndex + 1}`} <List.Accordion
expanded={expanded} title={`Account ${currentIndex + 1}`}
onPress={handlePress}> expanded={expanded}
{renderAccountItems()} onPress={handlePress}>
</List.Accordion> {renderAccountItems()}
</List.Accordion>
<View style={styles.addAccountButton}> <View style={styles.addAccountButton}>
<Button <Button
mode="contained" mode="contained"
onPress={addAccountHandler} onPress={addAccountHandler}
loading={isAccountCreating}> loading={isAccountCreating}>
{isAccountCreating ? 'Adding' : 'Add Account'} {isAccountCreating ? 'Adding' : 'Add Account'}
</Button> </Button>
</View> </View>
<View style={styles.addAccountButton}> <View style={styles.addAccountButton}>
<Button <Button
mode="contained" mode="contained"
onPress={() => { onPress={() => {
setHdDialog(true); setHdDialog(true);
setPathCode(network === 'eth' ? "m/44'/60'/" : "m/44'/118'/"); setPathCode(network === 'eth' ? "m/44'/60'/" : "m/44'/118'/");
}}> }}>
Add Account from HD path Add Account from HD path
</Button> </Button>
</View> </View>
<View style={styles.accountContainer}> <AccountDetails account={selectedAccounts[currentIndex]} />
<Text variant="bodyLarge">
<Text style={styles.highlight}>Address: </Text>
{selectedAccounts[currentIndex]?.address}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Public Key: </Text>
{selectedAccounts[currentIndex]?.pubKey}
</Text>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>HD Path: </Text>
{selectedAccounts[currentIndex]?.hdPath}
</Text>
</View>
<View style={styles.signLink}> <View style={styles.signLink}>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
navigation.navigate('SignMessage', { navigation.navigate('SignMessage', {
selectedNetwork: network, selectedNetwork: network,
accountInfo: selectedAccounts[currentIndex], accountInfo: selectedAccounts[currentIndex],
}); });
}}> }}>
<Text <Text
variant="titleSmall" variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}> style={[styles.hyperlink, { color: theme.colors.primary }]}>
Sign Message Sign Message
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View>
</View> </View>
</View> </ScrollView>
); );
}; };

View File

@ -4,13 +4,7 @@ import { Button, Dialog, Portal, Text } from 'react-native-paper';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import GridView from './Grid'; import GridView from './Grid';
import { CustomDialogProps } from '../types';
type CustomDialogProps = {
visible: boolean;
hideDialog: () => void;
contentText: string;
titleText?: string;
};
const DialogComponent = ({ const DialogComponent = ({
visible, visible,

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { ScrollView, View, Text } from 'react-native'; import { ScrollView, View, Text } from 'react-native';
import { Button, TextInput } from 'react-native-paper'; import { Button, TextInput } from 'react-native-paper';
import { addAccountFromHDPath } from '../utils/Accounts'; import { addAccountFromHDPath } from '../utils/accounts';
import { Account, PathState } from '../types'; import { Account, PathState } from '../types';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View } from 'react-native'; import { View, ActivityIndicator } from 'react-native';
import { Button } from 'react-native-paper'; import { Button, Text } from 'react-native-paper';
import { createWallet, resetWallet } from '../utils/Accounts'; import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
import { DialogComponent } from './Dialog'; import { DialogComponent } from './Dialog';
import { NetworkDropdown } from './NetworkDropdown'; import { NetworkDropdown } from './NetworkDropdown';
import { Account, AccountsState } from '../types'; import { Account, AccountsState } from '../types';
@ -18,6 +18,7 @@ const HomeScreen = () => {
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false); const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [network, setNetwork] = useState<string>('eth'); const [network, setNetwork] = useState<string>('eth');
const [currentIndex, setCurrentIndex] = useState<number>(0); const [currentIndex, setCurrentIndex] = useState<number>(0);
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
const [phrase, setPhrase] = useState(''); const [phrase, setPhrase] = useState('');
const [accounts, setAccounts] = useState<AccountsState>({ const [accounts, setAccounts] = useState<AccountsState>({
ethAccounts: [], ethAccounts: [],
@ -82,20 +83,39 @@ const HomeScreen = () => {
} }
}; };
useEffect(() => {
const fetchAccounts = async () => {
if (isAccountsFetched) {
return;
}
const { ethLoadedAccounts, cosmosLoadedAccounts } =
await retrieveAccounts();
if (ethLoadedAccounts && cosmosLoadedAccounts) {
setAccounts({
ethAccounts: ethLoadedAccounts,
cosmosAccounts: cosmosLoadedAccounts,
});
setIsWalletCreated(true);
}
setIsAccountsFetched(true);
};
fetchAccounts();
}, [isAccountsFetched]);
return ( return (
<View style={styles.appContainer}> <View style={styles.appContainer}>
<DialogComponent {!isAccountsFetched ? (
visible={walletDialog} <View style={styles.spinnerContainer}>
hideDialog={hideWalletDialog} <Text style={{ color: 'black', fontSize: 18, padding: 10 }}>
contentText={phrase} Loading...
/> </Text>
<ResetWalletDialog <ActivityIndicator size="large" color="#0000ff" />
visible={resetWalletDialog} </View>
hideDialog={hideResetDialog} ) : isWalletCreated ? (
onConfirm={confirmResetWallet}
/>
{isWalletCreated ? (
<> <>
<NetworkDropdown <NetworkDropdown
selectedNetwork={network} selectedNetwork={network}
@ -128,6 +148,16 @@ const HomeScreen = () => {
createWalletHandler={createWalletHandler} createWalletHandler={createWalletHandler}
/> />
)} )}
<DialogComponent
visible={walletDialog}
hideDialog={hideWalletDialog}
contentText={phrase}
/>
<ResetWalletDialog
visible={resetWalletDialog}
hideDialog={hideResetDialog}
onConfirm={confirmResetWallet}
/>
</View> </View>
); );
}; };

View File

@ -6,7 +6,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StackParamsList } from '../types'; import { StackParamsList } from '../types';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { signMessage } from '../utils/SignMessage'; import { signMessage } from '../utils/sign-message';
import AccountDetails from './AccountDetails';
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>; type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
@ -39,14 +40,7 @@ const SignMessage = ({ route }: SignProps) => {
</Text> </Text>
</View> </View>
<View style={styles.accountContainer}> <View style={styles.accountContainer}>
<Text variant="bodyLarge"> <AccountDetails account={account} />
<Text style={styles.highlight}>Address: </Text>
{account?.address}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Public Key: </Text>
{account?.pubKey}
</Text>
</View> </View>
</View> </View>

View File

@ -101,6 +101,11 @@ const styles = StyleSheet.create({
marginTop: 20, marginTop: 20,
width: 200, width: 200,
alignSelf: 'center', alignSelf: 'center',
spinnerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
}, },
}); });

View File

@ -1,12 +1,6 @@
export type StackParamsList = { export type StackParamsList = {
Laconic: undefined; Laconic: undefined;
SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined; SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined;
HDPath:
| {
updateIndex: (index: number) => void;
updateAccounts: (account: Account) => void;
}
| undefined;
}; };
export type Account = { export type Account = {
@ -68,6 +62,13 @@ export type HDPathDialogProps = {
updateAccounts: (account: Account) => void; updateAccounts: (account: Account) => void;
}; };
export type CustomDialogProps = {
visible: boolean;
hideDialog: () => void;
contentText: string;
titleText?: string;
};
export type GridViewProps = { export type GridViewProps = {
words: string[]; words: string[];
}; };

View File

@ -8,6 +8,7 @@ import { HDNode } from 'ethers/lib/utils';
import { import {
setInternetCredentials, setInternetCredentials,
resetInternetCredentials, resetInternetCredentials,
getInternetCredentials,
} from 'react-native-keychain'; } from 'react-native-keychain';
import { Account, WalletDetails } from '../types'; import { Account, WalletDetails } from '../types';
@ -18,6 +19,7 @@ import {
getHDPath, getHDPath,
getMnemonic, getMnemonic,
getNextAccountId, getNextAccountId,
getPathKey,
resetKeyServers, resetKeyServers,
updateAccountIndices, updateAccountIndices,
updateGlobalCounter, updateGlobalCounter,
@ -33,11 +35,13 @@ const createWallet = async (): Promise<WalletDetails> => {
const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0"); const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0");
const ethAddress = ethNode.address; const ethAddress = ethNode.address;
const cosmosAddress = (await getCosmosAccounts(mnemonic, `0'/0/0`)).data const cosmosAddress = (await getCosmosAccounts(mnemonic, "0'/0/0")).data
.address; .address;
const ethAccountInfo = `${`0'/0/0`},${ethNode.privateKey}`; const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${ethNode.publicKey
const cosmosAccountInfo = `${`0'/0/0`},${cosmosNode.privateKey}`; },${ethAddress}`;
const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${cosmosNode.publicKey
},${cosmosAddress}`;
await Promise.all([ await Promise.all([
setInternetCredentials( setInternetCredentials(
@ -82,26 +86,20 @@ const addAccount = async (network: string): Promise<Account | undefined> => {
const mnemonic = await getMnemonic(); const mnemonic = await getMnemonic();
const hdNode = HDNode.fromMnemonic(mnemonic); const hdNode = HDNode.fromMnemonic(mnemonic);
const id = await getNextAccountId(network); const id = await getNextAccountId(network);
const hdPath = getHDPath(network, id); const hdPath = getHDPath(network, `0'/0/${id}`);
const node = hdNode.derivePath(hdPath); const node = hdNode.derivePath(hdPath);
const pubKey = node.publicKey; const pubKey = node.publicKey;
const address = await getAddress(network, mnemonic, id); const address = await getAddress(network, mnemonic, `0'/0/${id}`);
await updateAccountIndices(network, id); await updateAccountIndices(network, id);
const { accountCounter, counterId } = await updateGlobalCounter(network); const { counterId } = await updateGlobalCounter(network);
await Promise.all([ await Promise.all([
resetInternetCredentials(`${network}:globalCounter`),
setInternetCredentials(
`${network}:globalCounter`,
`${network}Global`,
accountCounter,
),
setInternetCredentials( setInternetCredentials(
`${network}:keyServer:${counterId}`, `${network}:keyServer:${counterId}`,
`${network}:pathKey:${counterId}`, `${network}:pathKey:${counterId}`,
`0'/0/${id},${node.privateKey}`, `0'/0/${id},${node.privateKey},${node.publicKey},${address}`,
), ),
]); ]);
@ -125,20 +123,13 @@ const addAccountFromHDPath = async (
const { privKey, pubKey, address, network } = account; const { privKey, pubKey, address, network } = account;
const { accountCounter, counterId } = await updateGlobalCounter(network); const counterId = (await updateGlobalCounter(network)).counterId;
const updatedAccountCounter = `${accountCounter},${counterId.toString()}`;
await Promise.all([ await Promise.all([
resetInternetCredentials(`${network}:globalCounter`),
setInternetCredentials(
`${network}:globalCounter`,
`${network}Global`,
updatedAccountCounter,
),
setInternetCredentials( setInternetCredentials(
`${network}:keyServer:${counterId}`, `${network}:keyServer:${counterId}`,
`${network}:pathKey:${counterId}`, `${network}:pathKey:${counterId}`,
`${path},${privKey}`, `${path},${privKey},${pubKey},${address}`,
), ),
]); ]);
@ -148,6 +139,51 @@ const addAccountFromHDPath = async (
} }
}; };
const retrieveAccountsForNetwork = async (
network: string,
count: string,
): Promise<Account[]> => {
const elementsArray = count.split(',');
const loadedAccounts = await Promise.all(
elementsArray.map(async i => {
const pubKey = (await getPathKey(network, Number(i))).pubKey;
const address = (await getPathKey(network, Number(i))).address;
const path = (await getPathKey(network, Number(i))).path;
const hdPath = getHDPath(network, path);
const account: Account = {
counterId: Number(i),
pubKey: pubKey,
address: address,
hdPath: hdPath,
};
return account;
}),
);
return loadedAccounts;
};
const retrieveAccounts = async (): Promise<{
ethLoadedAccounts?: Account[];
cosmosLoadedAccounts?: Account[];
}> => {
const ethServer = await getInternetCredentials('eth:globalCounter');
const ethCounter = ethServer && ethServer.password;
const cosmosServer = await getInternetCredentials('cosmos:globalCounter');
const cosmosCounter = cosmosServer && cosmosServer.password;
const ethLoadedAccounts = ethCounter
? await retrieveAccountsForNetwork('eth', ethCounter)
: undefined;
const cosmosLoadedAccounts = cosmosCounter
? await retrieveAccountsForNetwork('cosmos', cosmosCounter)
: undefined;
return { ethLoadedAccounts, cosmosLoadedAccounts };
};
const resetWallet = async () => { const resetWallet = async () => {
try { try {
await Promise.all([ await Promise.all([
@ -165,4 +201,10 @@ const resetWallet = async () => {
} }
}; };
export { createWallet, addAccount, addAccountFromHDPath, resetWallet }; export {
createWallet,
addAccount,
addAccountFromHDPath,
retrieveAccounts,
resetWallet,
};

View File

@ -13,13 +13,13 @@ const signMessage = async ({
network, network,
accountId, accountId,
}: SignMessageParams): Promise<string | undefined> => { }: SignMessageParams): Promise<string | undefined> => {
const hdPath = (await getPathKey(network, accountId)).hdPath; const path = (await getPathKey(network, accountId)).path;
switch (network) { switch (network) {
case 'eth': case 'eth':
return await signEthMessage(message, accountId); return await signEthMessage(message, accountId);
case 'cosmos': case 'cosmos':
return await signCosmosMessage(message, hdPath); return await signCosmosMessage(message, path);
default: default:
throw new Error('Invalid wallet type'); throw new Error('Invalid wallet type');
} }
@ -43,11 +43,11 @@ const signEthMessage = async (
const signCosmosMessage = async ( const signCosmosMessage = async (
message: string, message: string,
hdPath: string, path: string,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
try { try {
const mnemonic = await getMnemonic(); const mnemonic = await getMnemonic();
const cosmosAccount = await getCosmosAccounts(mnemonic, hdPath); const cosmosAccount = await getCosmosAccounts(mnemonic, path);
const address = cosmosAccount.data.address; const address = cosmosAccount.data.address;
const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino( const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino(
address, address,

View File

@ -23,21 +23,21 @@ const getMnemonic = async (): Promise<string> => {
return mnemonic; return mnemonic;
}; };
const getHDPath = (network: string, id: number): string => { const getHDPath = (network: string, path: string): string => {
return network === 'eth' ? `m/44'/60'/0'/0/${id}` : `m/44'/118'/0'/0/${id}`; return network === 'eth' ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
}; };
const getAddress = async ( const getAddress = async (
network: string, network: string,
mnemonic: string, mnemonic: string,
id: number, path: string,
): Promise<string> => { ): Promise<string> => {
switch (network) { switch (network) {
case 'eth': case 'eth':
return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/0'/0/${id}`) return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/${path}`)
.address; .address;
case 'cosmos': case 'cosmos':
return (await getCosmosAccounts(mnemonic, `0'/0/${id}`)).data.address; return (await getCosmosAccounts(mnemonic, `${path}`)).data.address;
default: default:
throw new Error('Invalid wallet type'); throw new Error('Invalid wallet type');
} }
@ -45,10 +45,10 @@ const getAddress = async (
const getCosmosAccounts = async ( const getCosmosAccounts = async (
mnemonic: string, mnemonic: string,
hdPath: string, path: string,
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => { ): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [stringToPath(`m/44'/118'/${hdPath}`)], hdPaths: [stringToPath(`m/44'/118'/${path}`)],
}); });
const accountsData = await cosmosWallet.getAccounts(); const accountsData = await cosmosWallet.getAccounts();
@ -100,7 +100,12 @@ const accountInfoFromHDPath = async (
const getPathKey = async ( const getPathKey = async (
network: string, network: string,
accountId: number, accountId: number,
): Promise<{ hdPath: string; privKey: string }> => { ): Promise<{
path: string;
privKey: string;
pubKey: string;
address: string;
}> => {
const pathKeyStore = await getInternetCredentials( const pathKeyStore = await getInternetCredentials(
`${network}:keyServer:${accountId}`, `${network}:keyServer:${accountId}`,
); );
@ -111,10 +116,12 @@ const getPathKey = async (
const pathKeyVal = pathKeyStore.password; const pathKeyVal = pathKeyStore.password;
const pathkey = pathKeyVal.split(','); const pathkey = pathKeyVal.split(',');
const hdPath = pathkey[0]; const path = pathkey[0];
const privKey = pathkey[1]; const privKey = pathkey[1];
const pubKey = pathkey[2];
const address = pathkey[3];
return { hdPath, privKey }; return { path, privKey, pubKey, address };
}; };
const getGlobalCounter = async ( const getGlobalCounter = async (