laconic-wallet/utils.ts
2024-02-20 18:06:29 +05:30

404 lines
11 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 { 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[pathkey.length - 1];
switch (network) {
case 'eth':
return await signEthMessage(message, hdPath);
case 'cosmos':
return await signCosmosMessage(message, hdPath);
default:
throw new Error('Invalid wallet type');
}
};
const signEthMessage = async (
message: string,
hdPath: string,
): Promise<string | undefined> => {
try {
const keyCred = await getInternetCredentials(`eth:keyServer:${hdPath}`);
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,
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,
};