From 783758be3986a00145ef313ed51d4641a76980e3 Mon Sep 17 00:00:00 2001 From: IshaVenikar <145848618+IshaVenikar@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:32:25 +0530 Subject: [PATCH] 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 --- components/AccountDetails.tsx | 31 ++++++ components/Accounts.tsx | 118 ++++++++++------------ components/Dialog.tsx | 8 +- components/HDPath.tsx | 2 +- components/HomeScreen.tsx | 62 +++++++++--- components/SignMessage.tsx | 12 +-- styles/stylesheet.js | 5 + types.ts | 13 +-- utils/{Accounts.ts => accounts.ts} | 88 +++++++++++----- utils/{SignMessage.ts => sign-message.ts} | 8 +- utils/utils.ts | 27 +++-- 11 files changed, 235 insertions(+), 139 deletions(-) create mode 100644 components/AccountDetails.tsx rename utils/{Accounts.ts => accounts.ts} (64%) rename utils/{SignMessage.ts => sign-message.ts} (90%) diff --git a/components/AccountDetails.tsx b/components/AccountDetails.tsx new file mode 100644 index 0000000..157670f --- /dev/null +++ b/components/AccountDetails.tsx @@ -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 = ({ account }) => { + return ( + + + Address: + {account?.address} + + + Public Key: + {account?.pubKey} + + + HD Path: + {account?.hdPath} + + + ); +}; + +export default AccountDetails; diff --git a/components/Accounts.tsx b/components/Accounts.tsx index fa04c2a..2c58c08 100644 --- a/components/Accounts.tsx +++ b/components/Accounts.tsx @@ -1,12 +1,15 @@ 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 { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; + import { AccountsProps, StackParamsList, Account } from '../types'; -import { addAccount } from '../utils/Accounts'; +import { addAccount } from '../utils/accounts'; import styles from '../styles/stylesheet'; import HDPathDialog from './HDPathDialog'; +import AccountDetails from './AccountDetails'; const Accounts = ({ network, @@ -59,72 +62,61 @@ const Accounts = ({ )); return ( - - setHdDialog(false)} - updateAccounts={updateAccounts} - updateIndex={updateId} - pathCode={pathCode} - /> - - {renderAccountItems()} - + + + setHdDialog(false)} + updateAccounts={updateAccounts} + updateIndex={updateId} + pathCode={pathCode} + /> + + {renderAccountItems()} + - - - + + + - - - + + + - - - Address: - {selectedAccounts[currentIndex]?.address} - - - Public Key: - {selectedAccounts[currentIndex]?.pubKey} - - - HD Path: - {selectedAccounts[currentIndex]?.hdPath} - - + - - { - navigation.navigate('SignMessage', { - selectedNetwork: network, - accountInfo: selectedAccounts[currentIndex], - }); - }}> - - Sign Message - - + + { + navigation.navigate('SignMessage', { + selectedNetwork: network, + accountInfo: selectedAccounts[currentIndex], + }); + }}> + + Sign Message + + + - + ); }; diff --git a/components/Dialog.tsx b/components/Dialog.tsx index 2867b74..05fed95 100644 --- a/components/Dialog.tsx +++ b/components/Dialog.tsx @@ -4,13 +4,7 @@ import { Button, Dialog, Portal, Text } from 'react-native-paper'; import styles from '../styles/stylesheet'; import GridView from './Grid'; - -type CustomDialogProps = { - visible: boolean; - hideDialog: () => void; - contentText: string; - titleText?: string; -}; +import { CustomDialogProps } from '../types'; const DialogComponent = ({ visible, diff --git a/components/HDPath.tsx b/components/HDPath.tsx index 582d890..9b4d44f 100644 --- a/components/HDPath.tsx +++ b/components/HDPath.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { ScrollView, View, Text } from 'react-native'; import { Button, TextInput } from 'react-native-paper'; -import { addAccountFromHDPath } from '../utils/Accounts'; +import { addAccountFromHDPath } from '../utils/accounts'; import { Account, PathState } from '../types'; import styles from '../styles/stylesheet'; diff --git a/components/HomeScreen.tsx b/components/HomeScreen.tsx index e6dc587..32bc93a 100644 --- a/components/HomeScreen.tsx +++ b/components/HomeScreen.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; -import { View } from 'react-native'; -import { Button } from 'react-native-paper'; +import React, { useEffect, useState } from 'react'; +import { View, ActivityIndicator } from 'react-native'; +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 { NetworkDropdown } from './NetworkDropdown'; import { Account, AccountsState } from '../types'; @@ -18,6 +18,7 @@ const HomeScreen = () => { const [resetWalletDialog, setResetWalletDialog] = useState(false); const [network, setNetwork] = useState('eth'); const [currentIndex, setCurrentIndex] = useState(0); + const [isAccountsFetched, setIsAccountsFetched] = useState(false); const [phrase, setPhrase] = useState(''); const [accounts, setAccounts] = useState({ 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 ( - - - - {isWalletCreated ? ( + {!isAccountsFetched ? ( + + + Loading... + + + + ) : isWalletCreated ? ( <> { createWalletHandler={createWalletHandler} /> )} + + ); }; diff --git a/components/SignMessage.tsx b/components/SignMessage.tsx index dc3daf9..0f90654 100644 --- a/components/SignMessage.tsx +++ b/components/SignMessage.tsx @@ -6,7 +6,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { StackParamsList } from '../types'; import styles from '../styles/stylesheet'; -import { signMessage } from '../utils/SignMessage'; +import { signMessage } from '../utils/sign-message'; +import AccountDetails from './AccountDetails'; type SignProps = NativeStackScreenProps; @@ -39,14 +40,7 @@ const SignMessage = ({ route }: SignProps) => { - - Address: - {account?.address} - - - Public Key: - {account?.pubKey} - + diff --git a/styles/stylesheet.js b/styles/stylesheet.js index 962b199..0976861 100644 --- a/styles/stylesheet.js +++ b/styles/stylesheet.js @@ -101,6 +101,11 @@ const styles = StyleSheet.create({ marginTop: 20, width: 200, alignSelf: 'center', + spinnerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, }, }); diff --git a/types.ts b/types.ts index 46f7fe1..94b60ad 100644 --- a/types.ts +++ b/types.ts @@ -1,12 +1,6 @@ export type StackParamsList = { Laconic: undefined; SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined; - HDPath: - | { - updateIndex: (index: number) => void; - updateAccounts: (account: Account) => void; - } - | undefined; }; export type Account = { @@ -68,6 +62,13 @@ export type HDPathDialogProps = { updateAccounts: (account: Account) => void; }; +export type CustomDialogProps = { + visible: boolean; + hideDialog: () => void; + contentText: string; + titleText?: string; +}; + export type GridViewProps = { words: string[]; }; diff --git a/utils/Accounts.ts b/utils/accounts.ts similarity index 64% rename from utils/Accounts.ts rename to utils/accounts.ts index ec627ef..7c50e30 100644 --- a/utils/Accounts.ts +++ b/utils/accounts.ts @@ -8,6 +8,7 @@ import { HDNode } from 'ethers/lib/utils'; import { setInternetCredentials, resetInternetCredentials, + getInternetCredentials, } from 'react-native-keychain'; import { Account, WalletDetails } from '../types'; @@ -18,6 +19,7 @@ import { getHDPath, getMnemonic, getNextAccountId, + getPathKey, resetKeyServers, updateAccountIndices, updateGlobalCounter, @@ -33,11 +35,13 @@ const createWallet = async (): Promise => { const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0"); const ethAddress = ethNode.address; - const cosmosAddress = (await getCosmosAccounts(mnemonic, `0'/0/0`)).data + const cosmosAddress = (await getCosmosAccounts(mnemonic, "0'/0/0")).data .address; - const ethAccountInfo = `${`0'/0/0`},${ethNode.privateKey}`; - const cosmosAccountInfo = `${`0'/0/0`},${cosmosNode.privateKey}`; + const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${ethNode.publicKey + },${ethAddress}`; + const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${cosmosNode.publicKey + },${cosmosAddress}`; await Promise.all([ setInternetCredentials( @@ -82,26 +86,20 @@ const addAccount = async (network: string): Promise => { const mnemonic = await getMnemonic(); const hdNode = HDNode.fromMnemonic(mnemonic); const id = await getNextAccountId(network); - const hdPath = getHDPath(network, id); + const hdPath = getHDPath(network, `0'/0/${id}`); const node = hdNode.derivePath(hdPath); const pubKey = node.publicKey; - const address = await getAddress(network, mnemonic, id); + const address = await getAddress(network, mnemonic, `0'/0/${id}`); await updateAccountIndices(network, id); - const { accountCounter, counterId } = await updateGlobalCounter(network); + const { counterId } = await updateGlobalCounter(network); await Promise.all([ - resetInternetCredentials(`${network}:globalCounter`), - setInternetCredentials( - `${network}:globalCounter`, - `${network}Global`, - accountCounter, - ), setInternetCredentials( `${network}:keyServer:${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 { accountCounter, counterId } = await updateGlobalCounter(network); - const updatedAccountCounter = `${accountCounter},${counterId.toString()}`; + const counterId = (await updateGlobalCounter(network)).counterId; await Promise.all([ - resetInternetCredentials(`${network}:globalCounter`), - setInternetCredentials( - `${network}:globalCounter`, - `${network}Global`, - updatedAccountCounter, - ), setInternetCredentials( `${network}:keyServer:${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 => { + 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 () => { try { await Promise.all([ @@ -165,4 +201,10 @@ const resetWallet = async () => { } }; -export { createWallet, addAccount, addAccountFromHDPath, resetWallet }; +export { + createWallet, + addAccount, + addAccountFromHDPath, + retrieveAccounts, + resetWallet, +}; diff --git a/utils/SignMessage.ts b/utils/sign-message.ts similarity index 90% rename from utils/SignMessage.ts rename to utils/sign-message.ts index 8958f57..44794c5 100644 --- a/utils/SignMessage.ts +++ b/utils/sign-message.ts @@ -13,13 +13,13 @@ const signMessage = async ({ network, accountId, }: SignMessageParams): Promise => { - const hdPath = (await getPathKey(network, accountId)).hdPath; + const path = (await getPathKey(network, accountId)).path; switch (network) { case 'eth': return await signEthMessage(message, accountId); case 'cosmos': - return await signCosmosMessage(message, hdPath); + return await signCosmosMessage(message, path); default: throw new Error('Invalid wallet type'); } @@ -43,11 +43,11 @@ const signEthMessage = async ( const signCosmosMessage = async ( message: string, - hdPath: string, + path: string, ): Promise => { try { const mnemonic = await getMnemonic(); - const cosmosAccount = await getCosmosAccounts(mnemonic, hdPath); + const cosmosAccount = await getCosmosAccounts(mnemonic, path); const address = cosmosAccount.data.address; const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino( address, diff --git a/utils/utils.ts b/utils/utils.ts index 269134d..1602d0f 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -23,21 +23,21 @@ const getMnemonic = async (): Promise => { return mnemonic; }; -const getHDPath = (network: string, id: number): string => { - return network === 'eth' ? `m/44'/60'/0'/0/${id}` : `m/44'/118'/0'/0/${id}`; +const getHDPath = (network: string, path: string): string => { + return network === 'eth' ? `m/44'/60'/${path}` : `m/44'/118'/${path}`; }; const getAddress = async ( network: string, mnemonic: string, - id: number, + path: string, ): Promise => { switch (network) { case 'eth': - return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/0'/0/${id}`) + return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/${path}`) .address; case 'cosmos': - return (await getCosmosAccounts(mnemonic, `0'/0/${id}`)).data.address; + return (await getCosmosAccounts(mnemonic, `${path}`)).data.address; default: throw new Error('Invalid wallet type'); } @@ -45,10 +45,10 @@ const getAddress = async ( const getCosmosAccounts = async ( mnemonic: string, - hdPath: string, + path: string, ): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => { const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { - hdPaths: [stringToPath(`m/44'/118'/${hdPath}`)], + hdPaths: [stringToPath(`m/44'/118'/${path}`)], }); const accountsData = await cosmosWallet.getAccounts(); @@ -100,7 +100,12 @@ const accountInfoFromHDPath = async ( const getPathKey = async ( network: string, accountId: number, -): Promise<{ hdPath: string; privKey: string }> => { +): Promise<{ + path: string; + privKey: string; + pubKey: string; + address: string; +}> => { const pathKeyStore = await getInternetCredentials( `${network}:keyServer:${accountId}`, ); @@ -111,10 +116,12 @@ const getPathKey = async ( const pathKeyVal = pathKeyStore.password; const pathkey = pathKeyVal.split(','); - const hdPath = pathkey[0]; + const path = pathkey[0]; const privKey = pathkey[1]; + const pubKey = pathkey[2]; + const address = pathkey[3]; - return { hdPath, privKey }; + return { path, privKey, pubKey, address }; }; const getGlobalCounter = async (