forked from cerc-io/laconic-wallet
320 lines
8.8 KiB
TypeScript
320 lines
8.8 KiB
TypeScript
/* 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,
|
|
getInternetCredentials,
|
|
} from 'react-native-keychain';
|
|
|
|
import { Secp256k1HdWallet } from '@cosmjs/amino';
|
|
import { AccountData } from '@cosmjs/proto-signing';
|
|
import { stringToPath } from '@cosmjs/crypto';
|
|
|
|
import { Account, WalletDetails } from '../types';
|
|
import {
|
|
getHDPath,
|
|
getPathKey,
|
|
resetKeyServers,
|
|
updateGlobalCounter,
|
|
} from './misc';
|
|
|
|
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},${
|
|
ethNode.publicKey
|
|
},${ethAddress}`;
|
|
const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${
|
|
cosmosNode.publicKey
|
|
},${cosmosAddress}`;
|
|
|
|
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 id = await getNextAccountId(network);
|
|
const hdPath = getHDPath(network, `0'/0/${id}`);
|
|
const accounts = await addAccountFromHDPath(hdPath);
|
|
await updateAccountIndices(network, id);
|
|
return accounts;
|
|
} 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 counterId = (await updateGlobalCounter(network)).counterId;
|
|
|
|
await Promise.all([
|
|
setInternetCredentials(
|
|
`${network}:keyServer:${counterId}`,
|
|
`${network}:pathKey:${counterId}`,
|
|
`${path},${privKey},${pubKey},${address}`,
|
|
),
|
|
]);
|
|
|
|
return { counterId, pubKey, address, hdPath };
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const retrieveAccountsForNetwork = async (
|
|
network: string,
|
|
count: string,
|
|
): Promise<Account[]> => {
|
|
const elementsArray = count.split(',');
|
|
|
|
const loadedAccounts = await Promise.all(
|
|
elementsArray.map(async i => {
|
|
const pubKey = (await getPathKey(network, Number(i))).pubKey;
|
|
const address = (await getPathKey(network, Number(i))).address;
|
|
const path = (await getPathKey(network, Number(i))).path;
|
|
const hdPath = getHDPath(network, path);
|
|
|
|
const account: Account = {
|
|
counterId: Number(i),
|
|
pubKey: pubKey,
|
|
address: address,
|
|
hdPath: hdPath,
|
|
};
|
|
return account;
|
|
}),
|
|
);
|
|
|
|
return loadedAccounts;
|
|
};
|
|
|
|
const retrieveAccounts = async (): Promise<{
|
|
ethLoadedAccounts?: Account[];
|
|
cosmosLoadedAccounts?: Account[];
|
|
}> => {
|
|
const ethServer = await getInternetCredentials('eth:globalCounter');
|
|
const ethCounter = ethServer && ethServer.password;
|
|
const cosmosServer = await getInternetCredentials('cosmos:globalCounter');
|
|
const cosmosCounter = cosmosServer && cosmosServer.password;
|
|
|
|
const ethLoadedAccounts = ethCounter
|
|
? await retrieveAccountsForNetwork('eth', ethCounter)
|
|
: undefined;
|
|
const cosmosLoadedAccounts = cosmosCounter
|
|
? await retrieveAccountsForNetwork('cosmos', cosmosCounter)
|
|
: undefined;
|
|
|
|
return { ethLoadedAccounts, cosmosLoadedAccounts };
|
|
};
|
|
|
|
const retrieveSingleAccount = async (network: string, address: string) => {
|
|
let loadedAccounts;
|
|
|
|
switch (network) {
|
|
case 'eth':
|
|
const ethServer = await getInternetCredentials('eth:globalCounter');
|
|
const ethCounter = ethServer && ethServer.password;
|
|
|
|
if (ethCounter) {
|
|
loadedAccounts = await retrieveAccountsForNetwork(network, ethCounter);
|
|
}
|
|
break;
|
|
|
|
case 'cosmos':
|
|
const cosmosServer = await getInternetCredentials('cosmos:globalCounter');
|
|
const cosmosCounter = cosmosServer && cosmosServer.password;
|
|
|
|
if (cosmosCounter) {
|
|
loadedAccounts = await retrieveAccountsForNetwork(
|
|
network,
|
|
cosmosCounter,
|
|
);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (loadedAccounts) {
|
|
return loadedAccounts.find(account => account.address === address);
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
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 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 getCosmosAccounts = async (
|
|
mnemonic: string,
|
|
path: string,
|
|
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
|
|
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
|
|
hdPaths: [stringToPath(`m/44'/118'/${path}`)],
|
|
});
|
|
|
|
const accountsData = await cosmosWallet.getAccounts();
|
|
const data = accountsData[0];
|
|
|
|
return { cosmosWallet, data };
|
|
};
|
|
|
|
export {
|
|
createWallet,
|
|
addAccount,
|
|
addAccountFromHDPath,
|
|
retrieveAccounts,
|
|
retrieveSingleAccount,
|
|
resetWallet,
|
|
accountInfoFromHDPath,
|
|
getNextAccountId,
|
|
updateAccountIndices,
|
|
getCosmosAccounts,
|
|
};
|