forked from cerc-io/laconic-wallet
Merge pull request #20 from deep-stack/iv-add-account-from-hdpath
[WIP] Create account from hd path given by user
This commit is contained in:
commit
c1ca38ff23
@ -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<AccountsProps> = ({
|
||||
const Accounts = ({
|
||||
network,
|
||||
accounts,
|
||||
updateAccounts,
|
||||
currentIndex,
|
||||
updateIndex,
|
||||
}) => {
|
||||
updateIndex: updateId,
|
||||
}: AccountsProps) => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
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<AccountsProps> = ({
|
||||
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<AccountsProps> = ({
|
||||
const renderAccountItems = () =>
|
||||
selectedAccounts.map(account => (
|
||||
<List.Item
|
||||
key={account.id}
|
||||
title={`Account ${account.id + 1}`}
|
||||
key={account.counterId}
|
||||
title={`Account ${account.counterId + 1}`}
|
||||
onPress={() => {
|
||||
updateIndex(account.id);
|
||||
updateId(account.counterId);
|
||||
setExpanded(false);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<HDPathDialog
|
||||
visible={hdDialog}
|
||||
hideDialog={() => setHdDialog(false)}
|
||||
updateAccounts={updateAccounts}
|
||||
updateIndex={updateId}
|
||||
pathCode={pathCode}
|
||||
/>
|
||||
<List.Accordion
|
||||
title={`Account ${currentIndex + 1}`}
|
||||
expanded={expanded}
|
||||
@ -78,6 +83,17 @@ const Accounts: React.FC<AccountsProps> = ({
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={styles.addAccountButton}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
setHdDialog(true);
|
||||
setPathCode(network === 'eth' ? "m/44'/60'/" : "m/44'/118'/");
|
||||
}}>
|
||||
Add Account from HD path
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={styles.accountContainer}>
|
||||
<Text variant="bodyLarge">
|
||||
<Text style={styles.highlight}>Address: </Text>
|
||||
@ -87,6 +103,10 @@ const Accounts: React.FC<AccountsProps> = ({
|
||||
<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}>
|
||||
|
@ -12,11 +12,11 @@ type CustomDialogProps = {
|
||||
titleText?: string;
|
||||
};
|
||||
|
||||
const DialogComponent: React.FC<CustomDialogProps> = ({
|
||||
const DialogComponent = ({
|
||||
visible,
|
||||
hideDialog,
|
||||
contentText,
|
||||
}) => {
|
||||
}: CustomDialogProps) => {
|
||||
const words = contentText.split(' ');
|
||||
|
||||
return (
|
||||
|
@ -5,7 +5,7 @@ import { Text } from 'react-native-paper';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { GridViewProps } from '../types';
|
||||
|
||||
const GridView: React.FC<GridViewProps> = ({ words }) => {
|
||||
const GridView = ({ words }: GridViewProps) => {
|
||||
return (
|
||||
<View style={styles.gridContainer}>
|
||||
{words.map((word, index) => (
|
||||
|
64
components/HDPath.tsx
Normal file
64
components/HDPath.tsx
Normal file
@ -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<string>('');
|
||||
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 (
|
||||
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ color: 'black', fontSize: 18, padding: 10 }}>
|
||||
{pathCode}
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
onChangeText={setPath}
|
||||
value={path}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ marginTop: 20, width: 200, alignSelf: 'center' }}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={createFromHDPathHandler}
|
||||
loading={isAccountCreating}>
|
||||
{isAccountCreating ? 'Adding' : 'Add Account'}
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default HDPath;
|
30
components/HDPathDialog.tsx
Normal file
30
components/HDPathDialog.tsx
Normal file
@ -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 (
|
||||
<Portal>
|
||||
<Dialog visible={visible} onDismiss={hideDialog}>
|
||||
<Dialog.Title>Add account from HD path</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<HDPath
|
||||
pathCode={pathCode}
|
||||
updateIndex={updateIndex}
|
||||
updateAccounts={updateAccounts}
|
||||
hideDialog={hideDialog}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HDPathDialog;
|
@ -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<string>('eth');
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const [phrase, setPhrase] = useState('');
|
||||
|
||||
const [accounts, setAccounts] = useState<AccountsState>({
|
||||
ethAccounts: [],
|
||||
cosmosAccounts: [],
|
||||
@ -30,10 +29,8 @@ 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],
|
||||
@ -41,6 +38,7 @@ const HomeScreen = () => {
|
||||
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 (
|
||||
<View style={styles.appContainer}>
|
||||
<DialogComponent
|
||||
|
@ -5,7 +5,7 @@ import { List } from 'react-native-paper';
|
||||
import { NetworkDropdownProps } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const NetworkDropdown: React.FC<NetworkDropdownProps> = ({ updateNetwork }) => {
|
||||
const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<string>('Ethereum');
|
||||
|
||||
|
29
components/Section.tsx
Normal file
29
components/Section.tsx
Normal file
@ -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 (
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
},
|
||||
]}>
|
||||
{title}
|
||||
<Text style={styles.sectionDescription} />
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export { Section };
|
@ -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<StackParamsList, 'SignMessage'>;
|
||||
|
||||
@ -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) => {
|
||||
<View style={styles.accountInfo}>
|
||||
<View>
|
||||
<Text variant="headlineSmall">
|
||||
{account && `Account ${account.id + 1}`}
|
||||
{account && `Account ${account.counterId + 1}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.accountContainer}>
|
||||
|
17
types.ts
17
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[];
|
||||
};
|
||||
|
254
utils.ts
254
utils.ts
@ -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<WalletDetails> => {
|
||||
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<Account | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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 };
|
168
utils/Accounts.ts
Normal file
168
utils/Accounts.ts
Normal file
@ -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<WalletDetails> => {
|
||||
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<Account | undefined> => {
|
||||
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<Account | undefined> => {
|
||||
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 };
|
82
utils/SignMessage.ts
Normal file
82
utils/SignMessage.ts
Normal file
@ -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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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 };
|
213
utils/utils.ts
Normal file
213
utils/utils.ts
Normal file
@ -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<string> => {
|
||||
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<string> => {
|
||||
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<number> => {
|
||||
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<void> => {
|
||||
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,
|
||||
};
|
Loading…
Reference in New Issue
Block a user