diff --git a/components/Accounts.tsx b/components/Accounts.tsx index d5cb2e8..fa04c2a 100644 --- a/components/Accounts.tsx +++ b/components/Accounts.tsx @@ -1,26 +1,27 @@ import React, { useState } from 'react'; import { 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'; +import { addAccount } from '../utils/Accounts'; import styles from '../styles/stylesheet'; +import HDPathDialog from './HDPathDialog'; -const Accounts: React.FC = ({ +const Accounts = ({ network, accounts, updateAccounts, currentIndex, - updateIndex, -}) => { + updateIndex: updateId, +}: AccountsProps) => { const navigation = useNavigation>(); - const [expanded, setExpanded] = useState(false); const [isAccountCreating, setIsAccountCreating] = useState(false); + const [hdDialog, setHdDialog] = useState(false); + const [pathCode, setPathCode] = useState(''); + const theme = useTheme(); const handlePress = () => setExpanded(!expanded); @@ -28,11 +29,9 @@ const Accounts: React.FC = ({ setIsAccountCreating(true); const newAccount = await addAccount(network); setIsAccountCreating(false); - setExpanded(false); - if (newAccount) { updateAccounts(newAccount); - updateIndex(selectedAccounts[selectedAccounts.length - 1].id + 1); + updateId(newAccount.counterId); } }; @@ -50,18 +49,24 @@ const Accounts: React.FC = ({ const renderAccountItems = () => selectedAccounts.map(account => ( { - updateIndex(account.id); + updateId(account.counterId); setExpanded(false); }} /> )); - const theme = useTheme(); return ( + setHdDialog(false)} + updateAccounts={updateAccounts} + updateIndex={updateId} + pathCode={pathCode} + /> = ({ + + + + Address: @@ -87,6 +103,10 @@ const Accounts: React.FC = ({ Public Key: {selectedAccounts[currentIndex]?.pubKey} + + HD Path: + {selectedAccounts[currentIndex]?.hdPath} + diff --git a/components/Dialog.tsx b/components/Dialog.tsx index 9bef5e1..2867b74 100644 --- a/components/Dialog.tsx +++ b/components/Dialog.tsx @@ -12,11 +12,11 @@ type CustomDialogProps = { titleText?: string; }; -const DialogComponent: React.FC = ({ +const DialogComponent = ({ visible, hideDialog, contentText, -}) => { +}: CustomDialogProps) => { const words = contentText.split(' '); return ( diff --git a/components/Grid.tsx b/components/Grid.tsx index 752db7b..764e51c 100644 --- a/components/Grid.tsx +++ b/components/Grid.tsx @@ -5,7 +5,7 @@ import { Text } from 'react-native-paper'; import styles from '../styles/stylesheet'; import { GridViewProps } from '../types'; -const GridView: React.FC = ({ words }) => { +const GridView = ({ words }: GridViewProps) => { return ( {words.map((word, index) => ( diff --git a/components/HDPath.tsx b/components/HDPath.tsx new file mode 100644 index 0000000..6e9c6fa --- /dev/null +++ b/components/HDPath.tsx @@ -0,0 +1,64 @@ +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 { Account } from '../types'; + +const HDPath = ({ + pathCode, + updateAccounts, + updateIndex, + hideDialog, +}: { + pathCode: string; + updateIndex: (index: number) => void; + updateAccounts: (account: Account) => void; + hideDialog: () => void; +}) => { + const [path, setPath] = useState(''); + const [isAccountCreating, setIsAccountCreating] = useState(false); + + const createFromHDPathHandler = async () => { + setIsAccountCreating(true); + const hdPath = pathCode + path; + try { + const newAccount = await addAccountFromHDPath(hdPath); + if (newAccount) { + updateAccounts(newAccount); + updateIndex(newAccount.counterId); + hideDialog(); + } + } catch (error) { + console.error('Error creating account:', error); + } finally { + setIsAccountCreating(false); + } + }; + + return ( + + + + {pathCode} + + + + + + + + ); +}; + +export default HDPath; diff --git a/components/HDPathDialog.tsx b/components/HDPathDialog.tsx new file mode 100644 index 0000000..918d6ba --- /dev/null +++ b/components/HDPathDialog.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Portal, Dialog } from 'react-native-paper'; +import { HDPathDialogProps } from '../types'; +import HDPath from './HDPath'; + +const HDPathDialog = ({ + visible, + hideDialog, + updateIndex, + updateAccounts, + pathCode, +}: HDPathDialogProps) => { + return ( + + + Add account from HD path + + + + + + ); +}; + +export default HDPathDialog; diff --git a/components/HomeScreen.tsx b/components/HomeScreen.tsx index 9acdc9f..e6dc587 100644 --- a/components/HomeScreen.tsx +++ b/components/HomeScreen.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import { Alert, View } from 'react-native'; +import { View } from 'react-native'; import { Button } from 'react-native-paper'; -import { createWallet, resetWallet } from '../utils'; +import { createWallet, resetWallet } from '../utils/Accounts'; import { DialogComponent } from './Dialog'; import { NetworkDropdown } from './NetworkDropdown'; import { Account, AccountsState } from '../types'; @@ -19,7 +19,6 @@ const HomeScreen = () => { const [network, setNetwork] = useState('eth'); const [currentIndex, setCurrentIndex] = useState(0); const [phrase, setPhrase] = useState(''); - const [accounts, setAccounts] = useState({ ethAccounts: [], cosmosAccounts: [], @@ -30,17 +29,16 @@ const HomeScreen = () => { const createWalletHandler = async () => { setIsWalletCreating(true); - await new Promise(resolve => setTimeout(resolve, 2000)); const { mnemonic, ethAccounts, cosmosAccounts } = await createWallet(); - ethAccounts && - cosmosAccounts && + if (ethAccounts && cosmosAccounts) { setAccounts({ ethAccounts: [...accounts.ethAccounts, ethAccounts], cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts], }); - setWalletDialog(true); - setIsWalletCreated(true); - setPhrase(mnemonic); + setWalletDialog(true); + setIsWalletCreated(true); + setPhrase(mnemonic); + } }; const confirmResetWallet = async () => { @@ -80,9 +78,10 @@ const HomeScreen = () => { }); break; default: - Alert.alert('Select a valid network!'); + console.error('Select a valid network!'); } }; + return ( = ({ updateNetwork }) => { +const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => { const [expanded, setExpanded] = useState(false); const [selectedNetwork, setSelectedNetwork] = useState('Ethereum'); diff --git a/components/Section.tsx b/components/Section.tsx new file mode 100644 index 0000000..f1ab8fe --- /dev/null +++ b/components/Section.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren } from 'react'; +import { Text, View, useColorScheme } from 'react-native'; +import { Colors } from 'react-native/Libraries/NewAppScreen'; + +import styles from '../styles/stylesheet'; + +type SectionProps = PropsWithChildren<{ + title: string; +}>; + +const Section = ({ title }: SectionProps): React.JSX.Element => { + const isDarkMode = useColorScheme() === 'dark'; + return ( + + + {title} + + + + ); +}; + +export { Section }; diff --git a/components/SignMessage.tsx b/components/SignMessage.tsx index 64884f7..dc3daf9 100644 --- a/components/SignMessage.tsx +++ b/components/SignMessage.tsx @@ -5,8 +5,8 @@ import { Button, Text, TextInput } from 'react-native-paper'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { StackParamsList } from '../types'; -import { signMessage } from '../utils'; import styles from '../styles/stylesheet'; +import { signMessage } from '../utils/SignMessage'; type SignProps = NativeStackScreenProps; @@ -24,7 +24,7 @@ const SignMessage = ({ route }: SignProps) => { const signedMessage = await signMessage({ message, network, - accountId: account.id, + accountId: account.counterId, }); Alert.alert('Signature', signedMessage); } @@ -35,7 +35,7 @@ const SignMessage = ({ route }: SignProps) => { - {account && `Account ${account.id + 1}`} + {account && `Account ${account.counterId + 1}`} diff --git a/types.ts b/types.ts index ff85673..a21f152 100644 --- a/types.ts +++ b/types.ts @@ -1,12 +1,19 @@ export type StackParamsList = { Laconic: undefined; SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined; + HDPath: + | { + updateIndex: (index: number) => void; + updateAccounts: (account: Account) => void; + } + | undefined; }; export type Account = { - id: number; + counterId: number; pubKey: string; address: string; + hdPath: string; }; export type WalletDetails = { @@ -53,6 +60,14 @@ export type ResetDialogProps = { onConfirm: () => void; }; +export type HDPathDialogProps = { + pathCode: string; + visible: boolean; + hideDialog: () => void; + updateIndex: (index: number) => void; + updateAccounts: (account: Account) => void; +}; + export type GridViewProps = { words: string[]; }; diff --git a/utils.ts b/utils.ts deleted file mode 100644 index e8563a6..0000000 --- a/utils.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* Importing this library provides react native with a secure random source. -For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */ -import 'react-native-get-random-values'; -import '@ethersproject/shims'; - -import { Wallet, utils } from 'ethers'; -import { HDNode } from 'ethers/lib/utils'; -import { - setInternetCredentials, - getInternetCredentials, - resetInternetCredentials, -} from 'react-native-keychain'; - -import { AccountData, Secp256k1HdWallet } from '@cosmjs/amino'; -import { stringToPath } from '@cosmjs/crypto'; - -import { Account, SignMessageParams, WalletDetails } from './types'; - -const createWallet = async (): Promise => { - try { - const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16)); - await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic); - - const hdNode = HDNode.fromMnemonic(mnemonic); - const ethNode = hdNode.derivePath("m/44'/60'/0'/0/0"); - const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0"); - - const ethAddress = ethNode.address; - const cosmosAddress = (await getCosmosAccounts(mnemonic, 0)).data.address; - - await setInternetCredentials( - 'eth:keyServer:0', - 'eth:key:0', - ethNode.privateKey, - ); - await setInternetCredentials( - 'cosmos:keyServer:0', - 'cosmos:key:0', - cosmosNode.privateKey, - ); - - await setInternetCredentials('eth:accountIndices', 'ethAccount', '0'); - await setInternetCredentials('cosmos:accountIndices', 'cosmosAccount', '0'); - - const ethAccounts = { - id: 0, - pubKey: ethNode.publicKey, - address: ethAddress, - }; - - const cosmosAccounts = { - id: 0, - pubKey: cosmosNode.publicKey, - address: cosmosAddress, - }; - - return { mnemonic, ethAccounts, cosmosAccounts }; - } catch (error) { - console.error('Error creating HD wallet:', error); - return { mnemonic: '', ethAccounts: undefined, cosmosAccounts: undefined }; - } -}; - -const addAccount = async (network: string): Promise => { - try { - const mnemonicStore = await getInternetCredentials('mnemonicServer'); - if (!mnemonicStore) { - throw new Error('Mnemonic not found!'); - } - - const mnemonic = mnemonicStore.password; - const hdNode = HDNode.fromMnemonic(mnemonic); - - const idStore = await getInternetCredentials(`${network}:accountIndices`); - if (!idStore) { - throw new Error('Account id not found'); - } - - const accountIds = idStore.password; - const ids = accountIds.split(',').map(Number); - const id = ids[ids.length - 1] + 1; - - const derivationPath = - network === 'eth' ? `m/44'/60'/0'/0/${id}` : `m/44'/118'/0'/0/${id}`; - - const node = hdNode.derivePath(derivationPath); - const privKey = node.privateKey; - const pubKey = node.publicKey; - - let address: string; - - switch (network) { - case 'eth': - address = node.address; - break; - case 'cosmos': - address = (await getCosmosAccounts(mnemonic, id)).data.address; - break; - default: - throw new Error('Invalid wallet type'); - } - - await setInternetCredentials( - `${network}:keyServer:${id}`, - `${network}:key:${id}`, - privKey, - ); - - const accountIndicesKey = `${network}:accountIndices`; - const accountIndices = await getInternetCredentials(accountIndicesKey); - - if (!accountIndices) { - throw new Error('Account not found!'); - } - let indices = accountIndices.password; - indices += `,${id.toString()}`; - - await resetInternetCredentials(accountIndicesKey); - await setInternetCredentials( - accountIndicesKey, - `${network}Account`, - indices, - ); - - return { pubKey, address, id }; - } catch (error) { - console.error('Error creating account:', error); - } -}; - -const signMessage = async ({ - message, - network, - accountId, -}: SignMessageParams): Promise => { - switch (network) { - case 'eth': - return await signEthMessage(message, accountId); - case 'cosmos': - return await signCosmosMessage(message, accountId); - default: - throw new Error('Invalid wallet type'); - } -}; - -const signEthMessage = async ( - message: string, - id: number, -): Promise => { - try { - const keyCred = await getInternetCredentials(`eth:keyServer:${id}`); - - if (!keyCred) { - throw new Error('Failed to retrieve internet credentials'); - } - - const wallet = new Wallet(keyCred.password); - const signature = await wallet.signMessage(message); - - return signature; - } catch (error) { - console.error('Error signing Ethereum message:', error); - return undefined; - } -}; - -const signCosmosMessage = async ( - message: string, - id: number, -): Promise => { - try { - const mnemonicStore = await getInternetCredentials('mnemonicServer'); - - if (!mnemonicStore) { - throw new Error('Mnemonic not found'); - } - - const mnemonic = mnemonicStore.password; - const cosmosAccount = await getCosmosAccounts(mnemonic, id); - const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino( - cosmosAccount.data.address, - { - chain_id: '', - account_number: '0', - sequence: '0', - fee: { - gas: '0', - amount: [], - }, - msgs: [ - { - type: 'sign/MsgSignData', - value: { - signer: cosmosAccount.data.address, - data: btoa(message), - }, - }, - ], - memo: '', - }, - ); - - return cosmosSignature.signature.signature; - } catch (error) { - console.error('Error signing Cosmos message:', error); - return undefined; - } -}; - -const getCosmosAccounts = async ( - mnemonic: string, - id: number, -): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => { - const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { - hdPaths: [stringToPath(`m/44'/118'/0'/0/${id}`)], - }); - - const accountsData = await cosmosWallet.getAccounts(); - const data = accountsData[0]; - - return { cosmosWallet, data }; -}; - -const resetKeyServers = async (prefix: string) => { - const idStore = await getInternetCredentials(`${prefix}:accountIndices`); - if (!idStore) { - throw new Error('Account id not found.'); - } - - const accountIds = idStore.password; - const ids = accountIds.split(',').map(Number); - const id = ids[ids.length - 1]; - - for (let i = 0; i <= id; i++) { - await resetInternetCredentials(`${prefix}:keyServer:${i}`); - } -}; - -const resetWallet = async () => { - try { - await resetInternetCredentials('mnemonicServer'); - - await resetKeyServers('eth'); - await resetKeyServers('cosmos'); - - await resetInternetCredentials('eth:accountIndices'); - await resetInternetCredentials('cosmos:accountIndices'); - } catch (error) { - console.error('Error resetting wallet:', error); - throw error; - } -}; - -export { createWallet, addAccount, signMessage, resetWallet }; diff --git a/utils/Accounts.ts b/utils/Accounts.ts new file mode 100644 index 0000000..ec627ef --- /dev/null +++ b/utils/Accounts.ts @@ -0,0 +1,168 @@ +/* Importing this library provides react native with a secure random source. +For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */ +import 'react-native-get-random-values'; +import '@ethersproject/shims'; + +import { utils } from 'ethers'; +import { HDNode } from 'ethers/lib/utils'; +import { + setInternetCredentials, + resetInternetCredentials, +} from 'react-native-keychain'; + +import { Account, WalletDetails } from '../types'; +import { + accountInfoFromHDPath, + getAddress, + getCosmosAccounts, + getHDPath, + getMnemonic, + getNextAccountId, + resetKeyServers, + updateAccountIndices, + updateGlobalCounter, +} from './utils'; + +const createWallet = async (): Promise => { + try { + const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16)); + await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic); + + const hdNode = HDNode.fromMnemonic(mnemonic); + const ethNode = hdNode.derivePath("m/44'/60'/0'/0/0"); + const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0"); + + const ethAddress = ethNode.address; + const cosmosAddress = (await getCosmosAccounts(mnemonic, `0'/0/0`)).data + .address; + + const ethAccountInfo = `${`0'/0/0`},${ethNode.privateKey}`; + const cosmosAccountInfo = `${`0'/0/0`},${cosmosNode.privateKey}`; + + await Promise.all([ + setInternetCredentials( + 'eth:keyServer:0', + 'eth:pathKey:0', + ethAccountInfo, + ), + setInternetCredentials( + 'cosmos:keyServer:0', + 'cosmos:pathKey:0', + cosmosAccountInfo, + ), + setInternetCredentials('eth:accountIndices', 'ethCounter', '0'), + setInternetCredentials('cosmos:accountIndices', 'cosmosCounter', '0'), + setInternetCredentials('eth:globalCounter', 'ethGlobal', '0'), + setInternetCredentials('cosmos:globalCounter', 'cosmosGlobal', '0'), + ]); + + const ethAccounts = { + counterId: 0, + pubKey: ethNode.publicKey, + address: ethAddress, + hdPath: "m/44'/60'/0'/0/0", + }; + + const cosmosAccounts = { + counterId: 0, + pubKey: cosmosNode.publicKey, + address: cosmosAddress, + hdPath: "m/44'/118'/0'/0/0", + }; + + return { mnemonic, ethAccounts, cosmosAccounts }; + } catch (error) { + console.error('Error creating HD wallet:', error); + return { mnemonic: '', ethAccounts: undefined, cosmosAccounts: undefined }; + } +}; + +const addAccount = async (network: string): Promise => { + try { + const mnemonic = await getMnemonic(); + const hdNode = HDNode.fromMnemonic(mnemonic); + const id = await getNextAccountId(network); + const hdPath = getHDPath(network, id); + + const node = hdNode.derivePath(hdPath); + const pubKey = node.publicKey; + const address = await getAddress(network, mnemonic, id); + + await updateAccountIndices(network, id); + const { accountCounter, 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}`, + ), + ]); + + return { counterId, pubKey, address, hdPath }; + } catch (error) { + console.error('Error creating account:', error); + } +}; + +const addAccountFromHDPath = async ( + hdPath: string, +): Promise => { + try { + const account = await accountInfoFromHDPath(hdPath); + if (!account) { + throw new Error('Error while creating account'); + } + + const parts = hdPath.split('/'); + const path = parts.slice(-3).join('/'); + + const { privKey, pubKey, address, network } = account; + + const { accountCounter, counterId } = await updateGlobalCounter(network); + const updatedAccountCounter = `${accountCounter},${counterId.toString()}`; + + await Promise.all([ + resetInternetCredentials(`${network}:globalCounter`), + setInternetCredentials( + `${network}:globalCounter`, + `${network}Global`, + updatedAccountCounter, + ), + setInternetCredentials( + `${network}:keyServer:${counterId}`, + `${network}:pathKey:${counterId}`, + `${path},${privKey}`, + ), + ]); + + return { counterId, pubKey, address, hdPath }; + } catch (error) { + console.error(error); + } +}; + +const resetWallet = async () => { + try { + await Promise.all([ + resetInternetCredentials('mnemonicServer'), + resetKeyServers('eth'), + resetKeyServers('cosmos'), + resetInternetCredentials('eth:accountIndices'), + resetInternetCredentials('cosmos:accountIndices'), + resetInternetCredentials('eth:globalCounter'), + resetInternetCredentials('cosmos:globalCounter'), + ]); + } catch (error) { + console.error('Error resetting wallet:', error); + throw error; + } +}; + +export { createWallet, addAccount, addAccountFromHDPath, resetWallet }; diff --git a/utils/SignMessage.ts b/utils/SignMessage.ts new file mode 100644 index 0000000..8958f57 --- /dev/null +++ b/utils/SignMessage.ts @@ -0,0 +1,82 @@ +/* Importing this library provides react native with a secure random source. +For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */ +import 'react-native-get-random-values'; +import '@ethersproject/shims'; + +import { Wallet } from 'ethers'; + +import { SignMessageParams } from '../types'; +import { getCosmosAccounts, getMnemonic, getPathKey } from './utils'; + +const signMessage = async ({ + message, + network, + accountId, +}: SignMessageParams): Promise => { + const hdPath = (await getPathKey(network, accountId)).hdPath; + + switch (network) { + case 'eth': + return await signEthMessage(message, accountId); + case 'cosmos': + return await signCosmosMessage(message, hdPath); + default: + throw new Error('Invalid wallet type'); + } +}; + +const signEthMessage = async ( + message: string, + accountId: number, +): Promise => { + try { + const privKey = (await getPathKey('eth', accountId)).privKey; + const wallet = new Wallet(privKey); + const signature = await wallet.signMessage(message); + + return signature; + } catch (error) { + console.error('Error signing Ethereum message:', error); + return undefined; + } +}; + +const signCosmosMessage = async ( + message: string, + hdPath: string, +): Promise => { + try { + const mnemonic = await getMnemonic(); + const cosmosAccount = await getCosmosAccounts(mnemonic, hdPath); + const address = cosmosAccount.data.address; + const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino( + address, + { + chain_id: '', + account_number: '0', + sequence: '0', + fee: { + gas: '0', + amount: [], + }, + msgs: [ + { + type: 'sign/MsgSignData', + value: { + signer: address, + data: btoa(message), + }, + }, + ], + memo: '', + }, + ); + + return cosmosSignature.signature.signature; + } catch (error) { + console.error('Error signing Cosmos message:', error); + return undefined; + } +}; + +export { signMessage, signEthMessage, signCosmosMessage }; diff --git a/utils/utils.ts b/utils/utils.ts new file mode 100644 index 0000000..269134d --- /dev/null +++ b/utils/utils.ts @@ -0,0 +1,213 @@ +/* Importing this library provides react native with a secure random source. +For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */ +import 'react-native-get-random-values'; +import '@ethersproject/shims'; + +import { HDNode } from 'ethers/lib/utils'; +import { + getInternetCredentials, + resetInternetCredentials, + setInternetCredentials, +} from 'react-native-keychain'; + +import { AccountData, Secp256k1HdWallet } from '@cosmjs/amino'; +import { stringToPath } from '@cosmjs/crypto'; + +const getMnemonic = async (): Promise => { + const mnemonicStore = await getInternetCredentials('mnemonicServer'); + if (!mnemonicStore) { + throw new Error('Mnemonic not found!'); + } + + const mnemonic = mnemonicStore.password; + 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 getAddress = async ( + network: string, + mnemonic: string, + id: number, +): Promise => { + switch (network) { + case 'eth': + return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/0'/0/${id}`) + .address; + case 'cosmos': + return (await getCosmosAccounts(mnemonic, `0'/0/${id}`)).data.address; + default: + throw new Error('Invalid wallet type'); + } +}; + +const getCosmosAccounts = async ( + mnemonic: string, + hdPath: string, +): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => { + const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { + hdPaths: [stringToPath(`m/44'/118'/${hdPath}`)], + }); + + const accountsData = await cosmosWallet.getAccounts(); + const data = accountsData[0]; + + return { cosmosWallet, data }; +}; + +const accountInfoFromHDPath = async ( + hdPath: string, +): Promise< + | { privKey: string; pubKey: string; address: string; network: string } + | undefined +> => { + const mnemonicStore = await getInternetCredentials('mnemonicServer'); + if (!mnemonicStore) { + throw new Error('Mnemonic not found!'); + } + + const mnemonic = mnemonicStore.password; + const hdNode = HDNode.fromMnemonic(mnemonic); + const node = hdNode.derivePath(hdPath); + + const privKey = node.privateKey; + const pubKey = node.publicKey; + + const parts = hdPath.split('/'); + const path = parts.slice(-3).join('/'); + const coinType = parts[2]; + + let network: string; + let address: string; + + switch (coinType) { + case "60'": + network = 'eth'; + address = node.address; + break; + case "118'": + network = 'cosmos'; + address = (await getCosmosAccounts(mnemonic, path)).data.address; + break; + default: + throw new Error('Invalid wallet type'); + } + return { privKey, pubKey, address, network }; +}; + +const getPathKey = async ( + network: string, + accountId: number, +): Promise<{ hdPath: string; privKey: string }> => { + const pathKeyStore = await getInternetCredentials( + `${network}:keyServer:${accountId}`, + ); + + if (!pathKeyStore) { + throw new Error('Error while fetching counter'); + } + + const pathKeyVal = pathKeyStore.password; + const pathkey = pathKeyVal.split(','); + const hdPath = pathkey[0]; + const privKey = pathkey[1]; + + return { hdPath, privKey }; +}; + +const getGlobalCounter = async ( + network: string, +): Promise<{ + accountCounter: string; + counterIds: number[]; + counterId: number; +}> => { + const counterStore = await getInternetCredentials(`${network}:globalCounter`); + + if (!counterStore) { + throw new Error('Error while fetching counter'); + } + + let accountCounter = counterStore.password; + const counterIds = accountCounter.split(',').map(Number); + const counterId = counterIds[counterIds.length - 1] + 1; + + return { accountCounter, counterIds, counterId }; +}; + +const updateGlobalCounter = async ( + network: string, +): Promise<{ accountCounter: string; counterId: number }> => { + const globalCounterData = await getGlobalCounter(network); + const accountCounter = globalCounterData.accountCounter; + const counterId = globalCounterData.counterId; + const updatedAccountCounter = `${accountCounter},${counterId.toString()}`; + + await resetInternetCredentials(`${network}:globalCounter`); + await setInternetCredentials( + `${network}:globalCounter`, + `${network}Global`, + updatedAccountCounter, + ); + + return { accountCounter: updatedAccountCounter, counterId }; +}; + +const getNextAccountId = async (network: string): Promise => { + const idStore = await getInternetCredentials(`${network}:accountIndices`); + if (!idStore) { + throw new Error('Account id not found'); + } + + const accountIds = idStore.password; + const ids = accountIds.split(',').map(Number); + return ids[ids.length - 1] + 1; +}; + +const updateAccountIndices = async ( + network: string, + id: number, +): Promise => { + const idStore = await getInternetCredentials(`${network}:accountIndices`); + if (!idStore) { + throw new Error('Account id not found'); + } + + const updatedIndices = `${idStore.password},${id.toString()}`; + await resetInternetCredentials(`${network}:accountIndices`); + await setInternetCredentials( + `${network}:accountIndices`, + `${network}Counter`, + updatedIndices, + ); +}; + +const resetKeyServers = async (prefix: string) => { + const idStore = await getInternetCredentials(`${prefix}:accountIndices`); + if (!idStore) { + throw new Error('Account id not found.'); + } + + const accountIds = idStore.password; + const ids = accountIds.split(',').map(Number); + const id = ids[ids.length - 1]; + + for (let i = 0; i <= id; i++) { + await resetInternetCredentials(`${prefix}:keyServer:${i}`); + } +}; + +export { + accountInfoFromHDPath, + getCosmosAccounts, + getMnemonic, + getPathKey, + getNextAccountId, + updateGlobalCounter, + updateAccountIndices, + getHDPath, + getAddress, + resetKeyServers, +};