forked from cerc-io/laconic-wallet
Hard-code the prefix of the hd paths and refactor functions
This commit is contained in:
parent
63edfdd990
commit
9a6e52de90
@ -6,7 +6,7 @@ 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';
|
import { addAccount } from '../utils/Accounts';
|
||||||
import styles from '../styles/stylesheet';
|
import styles from '../styles/stylesheet';
|
||||||
import HDPathDialog from './HDPathDialog';
|
import HDPathDialog from './HDPathDialog';
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ const Accounts: React.FC<AccountsProps> = ({
|
|||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
||||||
const [hdDialog, setHdDialog] = useState(false);
|
const [hdDialog, setHdDialog] = useState(false);
|
||||||
|
const [pathCode, setPathCode] = useState('');
|
||||||
|
|
||||||
const handlePress = () => setExpanded(!expanded);
|
const handlePress = () => setExpanded(!expanded);
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ const Accounts: React.FC<AccountsProps> = ({
|
|||||||
hideDialog={() => setHdDialog(false)}
|
hideDialog={() => setHdDialog(false)}
|
||||||
updateAccounts={updateAccounts}
|
updateAccounts={updateAccounts}
|
||||||
updateIndex={updateId}
|
updateIndex={updateId}
|
||||||
|
pathCode={pathCode} // Pass pathCode here
|
||||||
/>
|
/>
|
||||||
<List.Accordion
|
<List.Accordion
|
||||||
title={`Account ${currentIndex + 1}`}
|
title={`Account ${currentIndex + 1}`}
|
||||||
@ -89,6 +91,16 @@ const Accounts: React.FC<AccountsProps> = ({
|
|||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setHdDialog(true);
|
setHdDialog(true);
|
||||||
|
switch (network) {
|
||||||
|
case 'eth':
|
||||||
|
setPathCode("m/44'/60'/");
|
||||||
|
break;
|
||||||
|
case 'cosmos':
|
||||||
|
setPathCode("m/44'/118'/");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setPathCode('');
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
Add Account from HD path
|
Add Account from HD path
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ScrollView, View } 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';
|
|
||||||
import { Account } from '../types';
|
import { Account } from '../types';
|
||||||
|
|
||||||
const HDPath = ({
|
const HDPath = ({
|
||||||
|
pathCode,
|
||||||
updateAccounts,
|
updateAccounts,
|
||||||
updateIndex,
|
updateIndex,
|
||||||
hideDialog,
|
hideDialog,
|
||||||
}: {
|
}: {
|
||||||
|
pathCode: string;
|
||||||
updateIndex: (index: number) => void;
|
updateIndex: (index: number) => void;
|
||||||
updateAccounts: (account: Account) => void;
|
updateAccounts: (account: Account) => void;
|
||||||
hideDialog: () => void;
|
hideDialog: () => void;
|
||||||
@ -19,7 +20,8 @@ const HDPath = ({
|
|||||||
|
|
||||||
const createFromHDPathHandler = async () => {
|
const createFromHDPathHandler = async () => {
|
||||||
setIsAccountCreating(true);
|
setIsAccountCreating(true);
|
||||||
const newAccount = await addAccountFromHDPath(path);
|
const hdPath = pathCode + path;
|
||||||
|
const newAccount = await addAccountFromHDPath(hdPath);
|
||||||
setIsAccountCreating(false);
|
setIsAccountCreating(false);
|
||||||
|
|
||||||
if (newAccount) {
|
if (newAccount) {
|
||||||
@ -31,11 +33,17 @@ const HDPath = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}>
|
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
<Text style={{ color: 'black', fontSize: 18, padding: 10 }}>
|
||||||
|
{pathCode}
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
onChangeText={text => setPath(text)}
|
onChangeText={text => setPath(text)}
|
||||||
value={path}
|
value={path}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
<View style={{ marginTop: 20, width: 200, alignSelf: 'center' }}>
|
<View style={{ marginTop: 20, width: 200, alignSelf: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
|
@ -8,6 +8,7 @@ const HDPathDialog = ({
|
|||||||
hideDialog,
|
hideDialog,
|
||||||
updateIndex,
|
updateIndex,
|
||||||
updateAccounts,
|
updateAccounts,
|
||||||
|
pathCode,
|
||||||
}: HDPathDialogProps) => {
|
}: HDPathDialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
@ -15,6 +16,7 @@ const HDPathDialog = ({
|
|||||||
<Dialog.Title>Add account from HD path</Dialog.Title>
|
<Dialog.Title>Add account from HD path</Dialog.Title>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<HDPath
|
<HDPath
|
||||||
|
pathCode={pathCode}
|
||||||
updateIndex={updateIndex}
|
updateIndex={updateIndex}
|
||||||
updateAccounts={updateAccounts}
|
updateAccounts={updateAccounts}
|
||||||
hideDialog={hideDialog}
|
hideDialog={hideDialog}
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { Alert, View } from 'react-native';
|
import { Alert, View } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
|
|
||||||
import { createWallet, resetWallet } from '../utils';
|
import { createWallet, resetWallet } 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';
|
||||||
|
@ -5,8 +5,8 @@ import { Button, Text, TextInput } from 'react-native-paper';
|
|||||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
|
||||||
import { StackParamsList } from '../types';
|
import { StackParamsList } from '../types';
|
||||||
import { signMessage } from '../utils';
|
|
||||||
import styles from '../styles/stylesheet';
|
import styles from '../styles/stylesheet';
|
||||||
|
import { signMessage } from '../utils/SignMessage';
|
||||||
|
|
||||||
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
|
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
|
||||||
|
|
||||||
|
1
types.ts
1
types.ts
@ -61,6 +61,7 @@ export type ResetDialogProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type HDPathDialogProps = {
|
export type HDPathDialogProps = {
|
||||||
|
pathCode: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
hideDialog: () => void;
|
hideDialog: () => void;
|
||||||
updateIndex: (index: number) => void;
|
updateIndex: (index: number) => void;
|
||||||
|
406
utils.ts
406
utils.ts
@ -1,406 +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'/0/0`)).data
|
|
||||||
.address;
|
|
||||||
|
|
||||||
const ethAccountInfo = `${0},${ethNode.privateKey}`;
|
|
||||||
const cosmosAccountInfo = `${0},${cosmosNode.privateKey}`;
|
|
||||||
|
|
||||||
await setInternetCredentials(
|
|
||||||
'eth:keyServer:0',
|
|
||||||
'eth:pathKey:0',
|
|
||||||
ethAccountInfo,
|
|
||||||
);
|
|
||||||
await setInternetCredentials(
|
|
||||||
'cosmos:keyServer:0',
|
|
||||||
'cosmos:pathKey:0',
|
|
||||||
cosmosAccountInfo,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setInternetCredentials('eth:accountIndices', 'ethCounter', '0');
|
|
||||||
await setInternetCredentials('cosmos:accountIndices', 'cosmosCounter', '0');
|
|
||||||
|
|
||||||
await setInternetCredentials('eth:globalCounter', 'ethGlobal', '0');
|
|
||||||
await 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 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 hdPath =
|
|
||||||
network === 'eth' ? `m/44'/60'/0'/0/${id}` : `m/44'/118'/0'/0/${id}`;
|
|
||||||
|
|
||||||
const node = hdNode.derivePath(hdPath);
|
|
||||||
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, `0'/0/${id}`)).data
|
|
||||||
.address;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Invalid wallet type');
|
|
||||||
}
|
|
||||||
|
|
||||||
let indices = idStore.password;
|
|
||||||
indices += `,${id.toString()}`;
|
|
||||||
|
|
||||||
await resetInternetCredentials(`${network}:accountIndices`);
|
|
||||||
await setInternetCredentials(
|
|
||||||
`${network}:accountIndices`,
|
|
||||||
`${network}Counter`,
|
|
||||||
indices,
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
accountCounter += `,${counterId.toString()}`;
|
|
||||||
|
|
||||||
await resetInternetCredentials(`${network}:globalCounter`);
|
|
||||||
|
|
||||||
await setInternetCredentials(
|
|
||||||
`${network}:globalCounter`,
|
|
||||||
`${network}Global`,
|
|
||||||
accountCounter,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setInternetCredentials(
|
|
||||||
`${network}:keyServer:${counterId}`,
|
|
||||||
`${network}:pathKey:${counterId}`,
|
|
||||||
`0'/0/${id},${privKey}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { counterId, pubKey, address, hdPath };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating account:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAccountFromHDPath = async (
|
|
||||||
hdPath: string,
|
|
||||||
): Promise<
|
|
||||||
| { counterId: number; pubKey: string; address: string; hdPath: string }
|
|
||||||
| undefined
|
|
||||||
> => {
|
|
||||||
try {
|
|
||||||
const account = await accountInfoFromHDPath(hdPath);
|
|
||||||
if (!account) {
|
|
||||||
throw new Error('Error while creating account');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { privKey, pubKey, address, network } = account;
|
|
||||||
|
|
||||||
const parts = hdPath.split('/');
|
|
||||||
const coinType = parts[2];
|
|
||||||
const path = parts.slice(-3).join('/');
|
|
||||||
|
|
||||||
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;
|
|
||||||
accountCounter += `,${counterId.toString()}`;
|
|
||||||
|
|
||||||
await resetInternetCredentials(`${network}:globalCounter`);
|
|
||||||
|
|
||||||
await setInternetCredentials(
|
|
||||||
`${network}:globalCounter`,
|
|
||||||
`${network}Global`,
|
|
||||||
accountCounter,
|
|
||||||
);
|
|
||||||
|
|
||||||
const accountInfo = `${path},${privKey}`;
|
|
||||||
|
|
||||||
switch (coinType) {
|
|
||||||
case "60'":
|
|
||||||
await setInternetCredentials(
|
|
||||||
`eth:keyServer:${counterId}`,
|
|
||||||
`eth:pathKey:${counterId}`,
|
|
||||||
accountInfo,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "118'":
|
|
||||||
await setInternetCredentials(
|
|
||||||
`cosmos:keyServer${counterId}`,
|
|
||||||
`cosmos:pathKey:${counterId}`,
|
|
||||||
accountInfo,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { counterId, pubKey, address, hdPath };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 signMessage = async ({
|
|
||||||
message,
|
|
||||||
network,
|
|
||||||
accountId,
|
|
||||||
}: SignMessageParams): Promise<string | undefined> => {
|
|
||||||
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];
|
|
||||||
|
|
||||||
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 keyCred = await getInternetCredentials(`eth:keyServer:${accountId}`);
|
|
||||||
|
|
||||||
if (!keyCred) {
|
|
||||||
throw new Error('Failed to retrieve internet credentials');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathKey = keyCred.password;
|
|
||||||
const privKeyVal = pathKey.split(',');
|
|
||||||
const privKey = privKeyVal[privKeyVal.length - 1];
|
|
||||||
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 mnemonicStore = await getInternetCredentials('mnemonicServer');
|
|
||||||
|
|
||||||
if (!mnemonicStore) {
|
|
||||||
throw new Error('Mnemonic not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mnemonic = mnemonicStore.password;
|
|
||||||
const cosmosAccount = await getCosmosAccounts(mnemonic, hdPath);
|
|
||||||
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,
|
|
||||||
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 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');
|
|
||||||
|
|
||||||
await resetInternetCredentials('eth:globalCounter');
|
|
||||||
await resetInternetCredentials('cosmos:globalCounter');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error resetting wallet:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
createWallet,
|
|
||||||
addAccount,
|
|
||||||
signMessage,
|
|
||||||
resetWallet,
|
|
||||||
addAccountFromHDPath,
|
|
||||||
};
|
|
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 };
|
80
utils/SignMessage.ts
Normal file
80
utils/SignMessage.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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 };
|
211
utils/utils.ts
Normal file
211
utils/utils.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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