Update keystore data structure (#88)

* Update keystore data structure (#83)

* Update create wallet and retrieve accounts functionality for updated data structure

* Refactor accounts state

* Use constant variable for cosmos

* Update add accounts incrementally and with custom HD path for updated data structure (#85)

* Change data structure

* Reset wallet change

* Fix signEthMessage

* Fix sign request

* Fix pairing with laconic pay dApp and sending tokens

* Add accounts to configured networks

* Update add account from hd path flow

* Handle review changes

---------

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>

* Remove network type state

* Refactor create wallet code (#89)

* Refactor create wallet code

* Create cosmos accounts with correct address prefix

* Use networks data from state while creating wallet

* Refactor code and add network id in types (#91)

* Refactor add new networks component

* Add selected network state in context

* Remove returning account from create wallet

---------

Co-authored-by: IshaVenikar <145848618+IshaVenikar@users.noreply.github.com>
This commit is contained in:
shreerang6921 2024-04-12 16:55:20 +05:30 committed by Nabarun Gogoi
parent 94bd8b6480
commit 3809ce88b1
19 changed files with 552 additions and 543 deletions

View File

@ -27,6 +27,7 @@ import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
import { getSignParamsMessage } from './utils/wallet-connect/Helpers';
import ApproveTransaction from './screens/ApproveTransaction';
import AddNetwork from './screens/AddNetwork';
import { COSMOS, EIP155 } from './utils/constants';
const Stack = createNativeStackNavigator<StackParamsList>();
@ -44,7 +45,7 @@ const App = (): React.JSX.Element => {
const onSessionProposal = useCallback(
async (proposal: SignClientTypes.EventArguments['session_proposal']) => {
if (!accounts.ethAccounts.length || !accounts.cosmosAccounts.length) {
if (!accounts.length || !accounts.length) {
const { id } = proposal;
await web3wallet!.rejectSession({
id,
@ -55,7 +56,7 @@ const App = (): React.JSX.Element => {
setModalVisible(true);
setCurrentProposal(proposal);
},
[accounts.ethAccounts, accounts.cosmosAccounts],
[accounts],
);
const onSessionRequest = useCallback(
@ -68,7 +69,6 @@ const App = (): React.JSX.Element => {
switch (request.method) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
navigation.navigate('ApproveTransaction', {
network: 'eth',
transaction: request.params[0],
requestEvent,
requestSessionData,
@ -77,7 +77,7 @@ const App = (): React.JSX.Element => {
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
navigation.navigate('SignRequest', {
network: 'eth',
namespace: EIP155,
address: request.params[1],
message: getSignParamsMessage(request.params),
requestEvent,
@ -103,7 +103,7 @@ const App = (): React.JSX.Element => {
),
};
navigation.navigate('SignRequest', {
network: 'cosmos',
namespace: COSMOS,
address: request.params.signerAddress,
message: JSON.stringify(message, undefined, 2),
requestEvent,
@ -113,7 +113,7 @@ const App = (): React.JSX.Element => {
break;
case 'cosmos_signAmino':
navigation.navigate('SignRequest', {
network: 'cosmos',
namespace: COSMOS,
address: request.params.signerAddress,
message: request.params.signDoc.memo,
requestEvent,
@ -124,7 +124,6 @@ const App = (): React.JSX.Element => {
case 'cosmos_sendTokens':
navigation.navigate('ApproveTransaction', {
network: 'cosmos',
transaction: request.params[0],
requestEvent,
requestSessionData,
@ -173,7 +172,6 @@ const App = (): React.JSX.Element => {
options={{
headerTitle: () => <Text variant="titleLarge">Sign Message</Text>,
}}
initialParams={{ selectedNetwork: 'Ethereum' }}
/>
<Stack.Screen
name="SignRequest"

View File

@ -16,17 +16,14 @@ import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
const Accounts = ({
network,
currentIndex,
updateIndex: updateId,
}: AccountsProps) => {
const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { accounts, setAccounts } = useAccounts();
const { networksData, currentChainId } = useNetworks();
const { selectedNetwork } = useNetworks();
const [expanded, setExpanded] = useState(false);
const [isAccountCreating, setIsAccountCreating] = useState(false);
const [hdDialog, setHdDialog] = useState(false);
@ -36,22 +33,7 @@ const Accounts = ({
const handlePress = () => setExpanded(!expanded);
const updateAccounts = (account: Account) => {
switch (network) {
case 'eth':
setAccounts({
...accounts,
ethAccounts: [...accounts.ethAccounts, account],
});
break;
case 'cosmos':
setAccounts({
...accounts,
cosmosAccounts: [...accounts.cosmosAccounts, account],
});
break;
default:
console.error('Select a valid network!');
}
setAccounts([...accounts, account]);
};
useEffect(() => {
@ -70,24 +52,22 @@ const Accounts = ({
: undefined,
);
const currentNetwork = networksData.find(
networkData => networkData.chainId === currentChainId,
);
const namespaceChainId = `${selectedNetwork?.namespace}:${selectedNetwork?.chainId}`;
let updatedNamespaces;
switch (currentNetwork?.networkType) {
case 'eth':
switch (selectedNetwork?.namespace) {
case EIP155:
updatedNamespaces = {
eip155: {
chains: [currentNetwork.chainId],
chains: [namespaceChainId],
// TODO: Debug optional namespace methods and events being required for approval
methods: [
...Object.values(EIP155_SIGNING_METHODS),
...(combinedNamespaces.eip155?.methods ?? []),
],
events: [...(combinedNamespaces.eip155?.events ?? [])],
accounts: accounts.ethAccounts.map(ethAccount => {
return `${currentChainId}:${ethAccount.address}`;
accounts: accounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`;
}),
},
cosmos: {
@ -98,17 +78,17 @@ const Accounts = ({
},
};
break;
case 'cosmos':
case COSMOS:
updatedNamespaces = {
cosmos: {
chains: [currentNetwork.chainId],
chains: [namespaceChainId],
methods: [
...Object.values(COSMOS_METHODS),
...(combinedNamespaces.cosmos?.methods ?? []),
],
events: [...(combinedNamespaces.cosmos?.events ?? [])],
accounts: accounts.cosmosAccounts.map(cosmosAccount => {
return `${currentChainId}:${cosmosAccount.address}`;
accounts: accounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`;
}),
},
eip155: {
@ -140,32 +120,21 @@ const Accounts = ({
const addAccountHandler = async () => {
setIsAccountCreating(true);
const newAccount = await addAccount(network);
const newAccount = await addAccount(selectedNetwork!);
setIsAccountCreating(false);
if (newAccount) {
updateAccounts(newAccount);
updateId(newAccount.counterId);
updateIndex(newAccount.index);
}
};
const selectedAccounts: Account[] = (() => {
switch (network) {
case 'eth':
return accounts.ethAccounts;
case 'cosmos':
return accounts.cosmosAccounts;
default:
return [];
}
})();
const renderAccountItems = () =>
selectedAccounts.map(account => (
accounts.map(account => (
<List.Item
key={account.counterId}
title={`Account ${account.counterId + 1}`}
key={account.index}
title={`Account ${account.index + 1}`}
onPress={() => {
updateId(account.counterId);
updateIndex(account.index);
setExpanded(false);
}}
/>
@ -178,7 +147,7 @@ const Accounts = ({
visible={hdDialog}
hideDialog={() => setHdDialog(false)}
updateAccounts={updateAccounts}
updateIndex={updateId}
updateIndex={updateIndex}
pathCode={pathCode}
/>
<List.Accordion
@ -202,20 +171,26 @@ const Accounts = ({
mode="contained"
onPress={() => {
setHdDialog(true);
setPathCode(network === 'eth' ? "m/44'/60'/" : "m/44'/118'/");
// TODO: Use coin type while adding from HD path
setPathCode(
selectedNetwork!.namespace === EIP155
? "m/44'/60'/"
: "m/44'/118'/",
);
}}>
Add Account from HD path
</Button>
</View>
<AccountDetails account={selectedAccounts[currentIndex]} />
<AccountDetails account={accounts[currentIndex]} />
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('SignMessage', {
selectedNetwork: network,
accountInfo: selectedAccounts[currentIndex],
selectedNamespace: selectedNetwork!.namespace,
selectedChainId: selectedNetwork!.chainId,
accountInfo: accounts[currentIndex],
});
}}>
<Text

View File

@ -3,7 +3,7 @@ import { ScrollView, View, Text } from 'react-native';
import { Button, TextInput } from 'react-native-paper';
import { addAccountFromHDPath } from '../utils/accounts';
import { Account, PathState } from '../types';
import { Account, NetworksDataState, PathState } from '../types';
import styles from '../styles/stylesheet';
const HDPath = ({
@ -11,11 +11,13 @@ const HDPath = ({
updateAccounts,
updateIndex,
hideDialog,
selectedNetwork,
}: {
pathCode: string;
updateIndex: (index: number) => void;
updateAccounts: (account: Account) => void;
hideDialog: () => void;
selectedNetwork: NetworksDataState;
}) => {
const [isAccountCreating, setIsAccountCreating] = useState(false);
const [path, setPath] = useState<PathState>({
@ -41,10 +43,10 @@ const HDPath = ({
pathCode +
`${path.firstNumber}'/${path.secondNumber}/${path.thirdNumber}`;
try {
const newAccount = await addAccountFromHDPath(hdPath);
const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork);
if (newAccount) {
updateAccounts(newAccount);
updateIndex(newAccount.counterId);
updateIndex(newAccount.index);
hideDialog();
}
} catch (error) {

View File

@ -1,7 +1,9 @@
import React from 'react';
import { Portal, Dialog } from 'react-native-paper';
import { HDPathDialogProps } from '../types';
import HDPath from './HDPath';
import { useNetworks } from '../context/NetworksContext';
const HDPathDialog = ({
visible,
@ -10,12 +12,15 @@ const HDPathDialog = ({
updateAccounts,
pathCode,
}: HDPathDialogProps) => {
const { selectedNetwork } = useNetworks();
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Title>Add account from HD path</Dialog.Title>
<Dialog.Content>
<HDPath
selectedNetwork={selectedNetwork!}
pathCode={pathCode}
updateIndex={updateIndex}
updateAccounts={updateAccounts}

View File

@ -6,7 +6,7 @@ import mergeWith from 'lodash/mergeWith';
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
import { AccountsState, PairingModalProps } from '../types';
import { PairingModalProps } from '../types';
import styles from '../styles/stylesheet';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useAccounts } from '../context/AccountsContext';
@ -14,6 +14,7 @@ import { useWalletConnect } from '../context/WalletConnectContext';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
const PairingModal = ({
visible,
@ -23,7 +24,7 @@ const PairingModal = ({
setToastVisible,
}: PairingModalProps) => {
const { accounts, currentIndex } = useAccounts();
const { networksData, currentChainId } = useNetworks();
const { selectedNetwork } = useNetworks();
const [isLoading, setIsLoading] = useState(false);
const dappName = currentProposal?.params?.proposer?.metadata.name;
@ -83,37 +84,21 @@ const PairingModal = ({
}
// Set selected account as the first account in supported namespaces
const sortedAccounts = Object.entries(accounts).reduce(
(acc: AccountsState, [key, value]) => {
let newValue = [...value];
const sortedAccounts = [
accounts[currentIndex],
...accounts.filter((account, index) => index !== currentIndex),
];
// TODO: Implement selectedAccount instead of currentIndex in AccountsContext
if (value.length > currentIndex) {
const currentAccount = newValue[currentIndex];
const remainingAccounts = newValue.filter(
(_, index) => index !== currentIndex,
);
newValue = [currentAccount, ...remainingAccounts];
}
acc[key as 'ethAccounts' | 'cosmosAccounts'] = newValue;
return acc;
},
{ ethAccounts: [], cosmosAccounts: [] },
);
const currentNetwork = networksData.find(
networkData => networkData.chainId === currentChainId,
);
const namespaceChainId = `${selectedNetwork?.namespace}:${selectedNetwork?.chainId}`;
const { optionalNamespaces, requiredNamespaces } = currentProposal.params;
// TODO: Check with other dApps
switch (currentNetwork?.networkType) {
case 'eth':
switch (selectedNetwork?.namespace) {
case EIP155:
return {
eip155: {
chains: [currentNetwork.chainId],
chains: [namespaceChainId],
// TODO: Debug optional namespace methods and events being required for approval
methods: [
...Object.values(EIP155_SIGNING_METHODS),
@ -124,8 +109,8 @@ const PairingModal = ({
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: sortedAccounts.ethAccounts.map(ethAccount => {
return `${currentChainId}:${ethAccount.address}`;
accounts: sortedAccounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`;
}),
},
cosmos: {
@ -135,10 +120,10 @@ const PairingModal = ({
accounts: [],
},
};
case 'cosmos':
case COSMOS:
return {
cosmos: {
chains: [currentNetwork.chainId],
chains: [namespaceChainId],
methods: [
...Object.values(COSMOS_METHODS),
...(optionalNamespaces.cosmos?.methods ?? []),
@ -148,8 +133,8 @@ const PairingModal = ({
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: sortedAccounts.cosmosAccounts.map(cosmosAccount => {
return `${currentChainId}:${cosmosAccount.address}`;
accounts: sortedAccounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`;
}),
},
eip155: {
@ -162,7 +147,7 @@ const PairingModal = ({
default:
break;
}
}, [accounts, currentProposal, networksData, currentChainId, currentIndex]);
}, [accounts, currentProposal, currentIndex, selectedNetwork]);
const namespaces = useMemo(() => {
return (

View File

@ -3,6 +3,7 @@ import { View } from 'react-native';
import { Text, List } from 'react-native-paper';
import styles from '../styles/stylesheet';
import { COSMOS, EIP155 } from '../utils/constants';
const SelectNetworkType = ({
updateNetworkType,
@ -16,7 +17,7 @@ const SelectNetworkType = ({
const handleNetworkPress = (network: string) => {
setSelectedNetwork(network);
updateNetworkType(network.toLowerCase());
updateNetworkType(network === 'ETH' ? EIP155 : COSMOS);
setExpanded(false);
};

View File

@ -1,21 +1,17 @@
import React, { createContext, useContext, useState } from 'react';
import { AccountsState } from '../types';
import { Account } from '../types';
const AccountsContext = createContext<{
accounts: AccountsState;
setAccounts: (account: AccountsState) => void;
accounts: Account[];
setAccounts: (account: Account[]) => void;
currentIndex: number;
setCurrentIndex: (index: number) => void;
networkType: string;
setNetworkType: (networkType: string) => void;
}>({
accounts: { ethAccounts: [], cosmosAccounts: [] },
accounts: [],
setAccounts: () => {},
currentIndex: 0,
setCurrentIndex: () => {},
networkType: '',
setNetworkType: () => {},
});
const useAccounts = () => {
@ -24,12 +20,8 @@ const useAccounts = () => {
};
const AccountsProvider = ({ children }: { children: any }) => {
const [accounts, setAccounts] = useState<AccountsState>({
ethAccounts: [],
cosmosAccounts: [],
});
const [accounts, setAccounts] = useState<Account[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [networkType, setNetworkType] = useState<string>('eth');
return (
<AccountsContext.Provider
@ -38,8 +30,6 @@ const AccountsProvider = ({ children }: { children: any }) => {
setAccounts,
currentIndex,
setCurrentIndex,
networkType,
setNetworkType,
}}>
{children}
</AccountsContext.Provider>

View File

@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
import { NetworksDataState } from '../types';
import { retrieveNetworksData, storeNetworkData } from '../utils/accounts';
import { DEFAULTNETWORKS } from '../utils/constants';
import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants';
const NetworksContext = createContext<{
currentIndex: number;
@ -11,8 +11,10 @@ const NetworksContext = createContext<{
setNetworksData: React.Dispatch<React.SetStateAction<NetworksDataState[]>>;
networkType: string;
setNetworkType: (networkType: string) => void;
currentChainId?: string;
setCurrentChainId: (currentChainId: string) => void;
selectedNetwork?: NetworksDataState;
setSelectedNetwork: React.Dispatch<
React.SetStateAction<NetworksDataState | undefined>
>;
}>({
currentIndex: 0,
setCurrentIndex: () => {},
@ -20,8 +22,8 @@ const NetworksContext = createContext<{
setNetworksData: () => {},
networkType: '',
setNetworkType: () => {},
currentChainId: undefined,
setCurrentChainId: () => {},
selectedNetwork: {} as NetworksDataState,
setSelectedNetwork: () => {},
});
const useNetworks = () => {
@ -32,20 +34,20 @@ const useNetworks = () => {
const NetworksProvider = ({ children }: { children: any }) => {
const [networksData, setNetworksData] = useState<NetworksDataState[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [networkType, setNetworkType] = useState<string>('eth');
const [currentChainId, setCurrentChainId] = useState<string>();
const [networkType, setNetworkType] = useState<string>(EIP155);
const [selectedNetwork, setSelectedNetwork] = useState<NetworksDataState>();
useEffect(() => {
const fetchData = async () => {
const retrievedNetworks = await retrieveNetworksData();
if (retrievedNetworks.length === 0) {
for (const defaultNetwork of DEFAULTNETWORKS) {
for (const defaultNetwork of DEFAULT_NETWORKS) {
await storeNetworkData(defaultNetwork);
}
}
const retrievedNewNetworks = await retrieveNetworksData();
setNetworksData(retrievedNewNetworks);
setCurrentChainId(retrievedNewNetworks[0].chainId);
setSelectedNetwork(retrievedNetworks[0]);
};
fetchData();
@ -60,8 +62,8 @@ const NetworksProvider = ({ children }: { children: any }) => {
setNetworksData,
networkType,
setNetworkType,
currentChainId,
setCurrentChainId,
selectedNetwork,
setSelectedNetwork,
}}>
{children}
</NetworksContext.Provider>

View File

@ -2,15 +2,22 @@ import React, { useCallback, useState } from 'react';
import { ScrollView } from 'react-native';
import { useForm, Controller } from 'react-hook-form';
import { TextInput, Button, HelperText } from 'react-native-paper';
import {
getInternetCredentials,
setInternetCredentials,
} from 'react-native-keychain';
import { HDNode } from 'ethers/lib/utils';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import styles from '../styles/stylesheet';
import { NetworksDataState, StackParamsList } from '../types';
import { NetworksDataState, NetworksFormData, StackParamsList } from '../types';
import { SelectNetworkType } from '../components/SelectNetworkType';
import { storeNetworkData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
import { getCosmosAccounts } from '../utils/accounts';
// TODO: Add validation to form inputs
const AddNetwork = () => {
@ -25,29 +32,79 @@ const AddNetwork = () => {
mode: 'onChange',
});
const { networksData, setNetworksData } = useNetworks();
const { setNetworksData } = useNetworks();
const [networkType, setNetworkType] = useState<string>('eth');
const [namespace, setNamespace] = useState<string>(EIP155);
const updateNetworkType = (newNetworkType: string) => {
setNetworkType(newNetworkType);
setNamespace(newNetworkType);
};
const submit = useCallback(
async (data: NetworksDataState) => {
const namespace = networkType === 'eth' ? 'eip155:' : 'cosmos:';
async (data: NetworksFormData) => {
const newNetworkData = {
...data,
chainId: `${namespace}${data.chainId}`,
networkType,
isDefault: false,
namespace,
};
setNetworksData([...networksData, newNetworkData]);
await storeNetworkData(newNetworkData);
const mnemonicServer = await getInternetCredentials('mnemonicServer');
const mnemonic = mnemonicServer && mnemonicServer.password;
if (!mnemonic) {
throw new Error('Mnemonic not found');
}
const hdNode = HDNode.fromMnemonic(mnemonic);
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (newNetworkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccounts(
mnemonic,
hdPath,
newNetworkData.addressPrefix,
)
).data.address;
break;
default:
throw new Error('Unsupported namespace');
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
const retrievedNetworksData = await storeNetworkData(newNetworkData);
setNetworksData(retrievedNetworksData);
await Promise.all([
setInternetCredentials(
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
'_',
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
'_',
'1',
),
setInternetCredentials(
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
'_',
'0',
),
]);
navigation.navigate('Laconic');
},
[navigation, networkType, networksData, setNetworksData],
[navigation, namespace, setNetworksData],
);
return (
@ -123,8 +180,25 @@ const AddNetwork = () => {
</>
)}
/>
<Controller
control={control}
name="coinType"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Coin Type"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">{errors.coinType?.message}</HelperText>
</>
)}
/>
<SelectNetworkType updateNetworkType={updateNetworkType} />
{networkType === 'eth' ? (
{namespace === EIP155 ? (
<Controller
control={control}
name="currencySymbol"
@ -184,23 +258,6 @@ const AddNetwork = () => {
</>
)}
/>
<Controller
control={control}
name="coinType"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Coin Type"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">{errors.coinType?.message}</HelperText>
</>
)}
/>
</>
)}
<Button

View File

@ -28,6 +28,7 @@ import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
@ -41,7 +42,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url;
const network = route.params.network;
// TODO: Remove and access namespace from requestEvent
const transaction = route.params.transaction;
const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId;
@ -54,18 +55,53 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useState<SigningStargateClient>();
const requestedChain = networksData.find(
networkData => networkData.chainId === chainId,
networkData =>
`${networkData.namespace}:${networkData.chainId}` === chainId,
);
const namespace = requestedChain!.namespace;
useEffect(() => {
if (namespace !== COSMOS) {
return;
}
const setClient = async () => {
if (!account) {
return;
}
const cosmosPrivKey = (
await getPathKey(
`${requestedChain?.namespace}:${requestedChain?.chainId}`,
account.index,
)
).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedChain?.addressPrefix,
);
const client = await SigningStargateClient.connectWithSigner(
requestedChain?.rpcUrl!,
sender,
);
setCosmosStargateClient(client);
};
setClient();
}, [account, requestedChain, chainId, namespace]);
const provider = useMemo(() => {
if (network === 'eth') {
if (namespace === EIP155) {
if (!requestedChain) {
throw new Error('Requested chain not supported');
}
return new providers.JsonRpcProvider(requestedChain.rpcUrl);
}
}, [requestedChain, network]);
}, [requestedChain, namespace]);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -73,9 +109,9 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const retrieveData = useCallback(
async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount(
network,
requestedChain!.namespace,
requestedChain!.chainId,
requestAddress,
requestedChain?.addressPrefix,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
@ -85,11 +121,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
setAccount(requestAccount);
setIsLoading(false);
},
[navigation, network, requestedChain],
[navigation, requestedChain],
);
const gasFees = useMemo(() => {
if (network === 'eth') {
if (namespace === EIP155) {
return BigNumber.from(transaction.gasLimit)
.mul(BigNumber.from(transaction.gasPrice))
.toString();
@ -99,7 +135,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
return cosmosFees.amount[0].amount;
}
}, [transaction, network]);
}, [transaction, namespace]);
useEffect(() => {
retrieveData(transaction.from!);
@ -115,9 +151,10 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
networksData,
requestEvent,
account,
network,
namespace!,
requestedChain!.chainId,
'',
network === 'eth' ? provider : cosmosStargateClient,
namespace === EIP155 ? provider : cosmosStargateClient,
);
const { topic } = requestEvent;
@ -140,7 +177,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useEffect(() => {
const getAccountBalance = async (account: Account) => {
if (network === 'eth') {
if (namespace === EIP155) {
const fetchedBalance =
provider && (await provider.getBalance(account.address));
@ -158,7 +195,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
if (account) {
getAccountBalance(account);
}
}, [account, provider, network, cosmosStargateClient, requestedChain]);
}, [account, provider, namespace, cosmosStargateClient, requestedChain]);
useEffect(() => {
navigation.setOptions({
@ -184,35 +221,6 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, route.name]);
useEffect(() => {
if (network !== 'cosmos') {
return;
}
const setClient = async () => {
if (!account) {
return;
}
const cosmosPrivKey = (await getPathKey('cosmos', account.counterId))
.privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedChain?.addressPrefix,
);
const client = await SigningStargateClient.connectWithSigner(
requestedChain?.rpcUrl!,
sender,
);
setCosmosStargateClient(client);
};
setClient();
}, [account, requestedChain, chainId, network]);
return (
<>
{isLoading ? (
@ -240,14 +248,12 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</View>
<DataBox
label={`Balance (${
requestedChain!.networkType === 'eth'
? 'wei'
: requestedChain!.nativeDenom
}`}
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
})`}
data={
balance === '' || balance === undefined
? 'Loading balance...'
: `${balance})`
: `${balance}`
}
/>
{transaction && (
@ -255,9 +261,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
<DataBox label="To" data={transaction.to!} />
<DataBox
label={`Amount (${
requestedChain!.networkType === 'eth'
? 'wei'
: requestedChain!.nativeDenom
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
})`}
data={BigNumber.from(
transaction.value?.toString(),
@ -265,13 +269,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
/>
<DataBox
label={`Gas Fees (${
requestedChain!.networkType === 'eth'
? 'wei'
: requestedChain!.nativeDenom
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
})`}
data={gasFees!}
/>
{network === 'eth' && (
{namespace === EIP155 && (
<DataBox label="Data" data={transaction.data!} />
)}
</View>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { View, ActivityIndicator, Image } from 'react-native';
import { Button, Text } from 'react-native-paper';
@ -29,22 +29,16 @@ const WCLogo = () => {
};
const HomeScreen = () => {
const {
accounts,
setAccounts,
currentIndex,
setCurrentIndex,
networkType,
setNetworkType,
} = useAccounts();
const { accounts, setAccounts, currentIndex, setCurrentIndex } =
useAccounts();
const { networksData, currentChainId, setCurrentChainId } = useNetworks();
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
const { setActiveSessions } = useWalletConnect();
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
useEffect(() => {
if (accounts.ethAccounts.length > 0) {
if (accounts.length > 0) {
navigation.setOptions({
headerRight: () => (
<Button onPress={() => navigation.navigate('WalletConnect')}>
@ -69,16 +63,27 @@ const HomeScreen = () => {
const hideWalletDialog = () => setWalletDialog(false);
const hideResetDialog = () => setResetWalletDialog(false);
const fetchAccounts = useCallback(async () => {
if (!selectedNetwork) {
return;
}
const loadedAccounts = await retrieveAccounts(selectedNetwork);
if (loadedAccounts) {
setAccounts(loadedAccounts);
setIsWalletCreated(true);
}
setIsAccountsFetched(true);
}, [selectedNetwork, setAccounts]);
const createWalletHandler = async () => {
setIsWalletCreating(true);
const { mnemonic, ethAccounts, cosmosAccounts } = await createWallet();
if (ethAccounts && cosmosAccounts) {
setAccounts({
ethAccounts: [...accounts.ethAccounts, ethAccounts],
cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts],
});
const mnemonic = await createWallet(networksData);
if (mnemonic) {
fetchAccounts();
setWalletDialog(true);
setIsWalletCreated(true);
setPhrase(mnemonic);
}
};
@ -87,10 +92,7 @@ const HomeScreen = () => {
await resetWallet();
setIsWalletCreated(false);
setIsWalletCreating(false);
setAccounts({
ethAccounts: [],
cosmosAccounts: [],
});
setAccounts([]);
setCurrentIndex(0);
const sessions = web3wallet!.getActiveSessions();
@ -103,12 +105,10 @@ const HomeScreen = () => {
setActiveSessions({});
hideResetDialog();
setNetworkType('eth');
};
const updateNetwork = (networksData: NetworksDataState) => {
setNetworkType(networksData.networkType);
setCurrentChainId(networksData.chainId);
setSelectedNetwork(networksData);
setCurrentIndex(0);
};
@ -117,34 +117,8 @@ const HomeScreen = () => {
};
useEffect(() => {
const fetchAccounts = async () => {
if (!currentChainId) {
return;
}
const currentNetwork = networksData.find(
networkData => networkData.chainId === currentChainId,
);
if (!currentNetwork) {
throw new Error('Current Network not found');
}
const { ethLoadedAccounts, cosmosLoadedAccounts } =
await retrieveAccounts(currentNetwork.addressPrefix);
if (cosmosLoadedAccounts && ethLoadedAccounts) {
setAccounts({
ethAccounts: ethLoadedAccounts,
cosmosAccounts: cosmosLoadedAccounts,
});
setIsWalletCreated(true);
}
setIsAccountsFetched(true);
};
fetchAccounts();
}, [currentChainId, networksData, setAccounts]);
}, [networksData, setAccounts, selectedNetwork, fetchAccounts]);
return (
<View style={styles.appContainer}>
@ -157,11 +131,7 @@ const HomeScreen = () => {
<>
<NetworkDropdown updateNetwork={updateNetwork} />
<View style={styles.accountComponent}>
<Accounts
network={networkType}
currentIndex={currentIndex}
updateIndex={updateIndex}
/>
<Accounts currentIndex={currentIndex} updateIndex={updateIndex} />
</View>
<View style={styles.resetContainer}>
<Button

View File

@ -12,7 +12,8 @@ import AccountDetails from '../components/AccountDetails';
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
const SignMessage = ({ route }: SignProps) => {
const network = route.params.selectedNetwork;
const namespace = route.params.selectedNamespace;
const chainId = route.params.selectedChainId;
const account = route.params.accountInfo;
const [message, setMessage] = useState<string>('');
@ -20,8 +21,9 @@ const SignMessage = ({ route }: SignProps) => {
const signMessageHandler = async () => {
const signedMessage = await signMessage({
message,
network,
accountId: account.counterId,
namespace,
chainId,
accountId: account.index,
});
Alert.alert('Signature', signedMessage);
};
@ -31,7 +33,7 @@ const SignMessage = ({ route }: SignProps) => {
<View style={styles.accountInfo}>
<View>
<Text variant="titleMedium">
{account && `Account ${account.counterId + 1}`}
{account && `Account ${account.index + 1}`}
</Text>
</View>
<View style={styles.accountContainer}>

View File

@ -26,7 +26,7 @@ import { useNetworks } from '../context/NetworksContext';
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
const SignRequest = ({ route }: SignRequestProps) => {
const { networksData } = useNetworks();
const { networksData, selectedNetwork } = useNetworks();
const requestSession = route.params.requestSessionData;
const requestName = requestSession?.peer?.metadata?.name;
@ -35,7 +35,8 @@ const SignRequest = ({ route }: SignRequestProps) => {
const [account, setAccount] = useState<Account>();
const [message, setMessage] = useState<string>('');
const [network, setNetwork] = useState<string>('');
const [namespace, setNamespace] = useState<string>('');
const [chainId, setChainId] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
@ -68,20 +69,15 @@ const SignRequest = ({ route }: SignRequestProps) => {
const retrieveData = useCallback(
async (
requestNetwork: string,
requestNamespace: string,
requestChainId: string,
requestAddress: string,
requestMessage: string,
) => {
const currentChain = networksData.find(networkData => {
if (networkData.addressPrefix) {
return requestAddress.includes(networkData.addressPrefix);
}
});
const requestAccount = await retrieveSingleAccount(
requestNetwork,
requestNamespace,
requestChainId,
requestAddress,
currentChain?.addressPrefix,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
@ -94,21 +90,23 @@ const SignRequest = ({ route }: SignRequestProps) => {
if (requestMessage !== message) {
setMessage(decodeURIComponent(requestMessage));
}
if (requestNetwork !== network) {
setNetwork(requestNetwork);
if (requestNamespace !== namespace) {
setNamespace(requestNamespace);
}
if (requestChainId !== chainId) {
setChainId(requestChainId);
}
setIsLoading(false);
},
[account, message, navigation, network, networksData],
[account, message, navigation, namespace, chainId],
);
const sanitizePath = useCallback(
(path: string) => {
const regex = /^\/sign\/(eth|cosmos)\/(.+)\/(.+)$/;
const regex = /^\/sign\/(eip155|cosmos)\/(.+)\/(.+)$/;
const match = path.match(regex);
if (match) {
const [network, address, message] = match;
const [, network, address, message] = match;
return {
network,
address,
@ -127,18 +125,28 @@ const SignRequest = ({ route }: SignRequestProps) => {
const sanitizedRoute = sanitizePath(route.path);
sanitizedRoute &&
retrieveData(
route.params.network,
route.params.address,
route.params.message,
sanitizedRoute.network,
// TODO: Take chainId from route.path
selectedNetwork!.chainId!,
sanitizedRoute.address,
sanitizedRoute.message,
);
return;
}
const requestEvent = route.params.requestEvent;
const requestChainId = requestEvent?.params.chainId;
const requestedChain = networksData.find(
networkData => networkData.chainId === requestChainId?.split(':')[1],
);
retrieveData(
route.params.network,
requestedChain!.namespace,
requestedChain!.chainId,
route.params.address,
route.params.message,
);
}, [retrieveData, sanitizePath, route]);
}, [retrieveData, sanitizePath, route, networksData, selectedNetwork]);
const handleWalletConnectRequest = async () => {
const { requestEvent } = route.params || {};
@ -155,7 +163,8 @@ const SignRequest = ({ route }: SignRequestProps) => {
networksData,
requestEvent,
account,
network,
namespace,
chainId,
message,
);
@ -170,8 +179,9 @@ const SignRequest = ({ route }: SignRequestProps) => {
if (message) {
const signedMessage = await signMessage({
message,
network,
accountId: account.counterId,
namespace,
chainId,
accountId: account.index,
});
Alert.alert('Signature', signedMessage);
}

View File

@ -5,16 +5,19 @@ import { Web3WalletTypes } from '@walletconnect/web3wallet';
export type StackParamsList = {
Laconic: undefined;
SignMessage: { selectedNetwork: string; accountInfo: Account };
SignMessage: {
selectedNamespace: string;
selectedChainId: string;
accountInfo: Account;
};
SignRequest: {
network: string;
namespace: string;
address: string;
message: string;
requestEvent?: Web3WalletTypes.SessionRequest;
requestSessionData?: SessionTypes.Struct;
};
ApproveTransaction: {
network: string;
transaction: PopulatedTransaction;
requestEvent: Web3WalletTypes.SessionRequest;
requestSessionData: SessionTypes.Struct;
@ -26,20 +29,13 @@ export type StackParamsList = {
};
export type Account = {
counterId: number;
index: number;
pubKey: string;
address: string;
hdPath: string;
};
export type WalletDetails = {
mnemonic: string;
ethAccounts: Account | undefined;
cosmosAccounts: Account | undefined;
};
export type AccountsProps = {
network: string;
currentIndex: number;
updateIndex: (index: number) => void;
};
@ -48,27 +44,27 @@ export type NetworkDropdownProps = {
updateNetwork: (networksData: NetworksDataState) => void;
};
export type AccountsState = {
ethAccounts: Account[];
cosmosAccounts: Account[];
};
export type NetworksDataState = {
export type NetworksFormData = {
networkName: string;
rpcUrl: string;
chainId: string;
currencySymbol?: string;
blockExplorerUrl?: string;
networkType: string;
namespace: string;
nativeDenom?: string;
addressPrefix?: string;
coinType?: string;
isDefault: boolean;
};
export interface NetworksDataState extends NetworksFormData {
networkId: string;
}
export type SignMessageParams = {
message: string;
network: string;
namespace: string;
chainId: string;
accountId: number;
};

View File

@ -16,78 +16,76 @@ import { Secp256k1HdWallet } from '@cosmjs/amino';
import { AccountData } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
import { Account, NetworksDataState, WalletDetails } from '../types';
import { Account, NetworksDataState, NetworksFormData } from '../types';
import {
getHDPath,
getPathKey,
resetKeyServers,
updateGlobalCounter,
updateAccountIndices,
} from './misc';
import { COSMOS, EIP155 } from './constants';
const createWallet = async (): Promise<WalletDetails> => {
try {
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
const createWallet = async (
networksData: NetworksDataState[],
): Promise<string> => {
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 hdNode = HDNode.fromMnemonic(mnemonic);
const ethAddress = ethNode.address;
const cosmosAddress = (await getCosmosAccounts(mnemonic, "0'/0/0")).data
.address;
for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${
ethNode.publicKey
},${ethAddress}`;
const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${
cosmosNode.publicKey
},${cosmosAddress}`;
switch (network.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccounts(mnemonic, hdPath, network.addressPrefix)
).data.address;
break;
default:
throw new Error('Unsupported namespace');
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
await Promise.all([
setInternetCredentials(
'eth:keyServer:0',
'eth:pathKey:0',
ethAccountInfo,
`accounts/${network.namespace}:${network.chainId}/0`,
'_',
accountInfo,
),
setInternetCredentials(
'cosmos:keyServer:0',
'cosmos:pathKey:0',
cosmosAccountInfo,
`addAccountCounter/${network.namespace}:${network.chainId}`,
'_',
'1',
),
setInternetCredentials(
`accountIndices/${network.namespace}:${network.chainId}`,
'_',
'0',
),
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 };
}
return mnemonic;
};
const addAccount = async (network: string): Promise<Account | undefined> => {
const addAccount = async (
networkData: NetworksDataState,
): 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);
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
const id = await getNextAccountId(namespaceChainId);
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
const accounts = await addAccountFromHDPath(hdPath, networkData);
await updateAccountCounter(namespaceChainId, id);
return accounts;
} catch (error) {
console.error('Error creating account:', error);
@ -96,37 +94,40 @@ const addAccount = async (network: string): Promise<Account | undefined> => {
const addAccountFromHDPath = async (
hdPath: string,
networkData: NetworksDataState,
): Promise<Account | undefined> => {
try {
const account = await accountInfoFromHDPath(hdPath);
const account = await accountInfoFromHDPath(
hdPath,
networkData.addressPrefix,
);
if (!account) {
throw new Error('Error while creating account');
}
const parts = hdPath.split('/');
const path = parts.slice(-3).join('/');
const { privKey, pubKey, address } = account;
const { privKey, pubKey, address, network } = account;
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
const counterId = (await updateGlobalCounter(network)).counterId;
const index = (await updateAccountIndices(namespaceChainId)).index;
await Promise.all([
setInternetCredentials(
`${network}:keyServer:${counterId}`,
`${network}:pathKey:${counterId}`,
`${path},${privKey},${pubKey},${address}`,
`accounts/${namespaceChainId}/${index}`,
'_',
`${hdPath},${privKey},${pubKey},${address}`,
),
]);
return { counterId, pubKey, address, hdPath };
return { index, pubKey, address, hdPath };
} catch (error) {
console.error(error);
}
};
const storeNetworkData = async (
networkData: NetworksDataState,
): Promise<void> => {
networkData: NetworksFormData,
): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
const retrievedNetworks =
networks && networks.password ? JSON.parse(networks.password) : [];
@ -135,7 +136,7 @@ const storeNetworkData = async (
networkId = retrievedNetworks[retrievedNetworks.length - 1].networkId + 1;
}
const updatedNetworks = [
const updatedNetworks: NetworksDataState[] = [
...retrievedNetworks,
{ networkId: networkId, ...networkData },
];
@ -144,6 +145,8 @@ const storeNetworkData = async (
'_',
JSON.stringify(updatedNetworks),
);
return updatedNetworks;
};
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
@ -155,24 +158,23 @@ const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
};
export const retrieveAccountsForNetwork = async (
network: string,
namespaceChainId: string,
accountsIndices: string,
prefix: string = 'cosmos',
): Promise<Account[]> => {
const accountsIndexArray = accountsIndices.split(',');
const loadedAccounts = await Promise.all(
accountsIndexArray.map(async i => {
const pubKey = (await getPathKey(network, Number(i), prefix)).pubKey;
const address = (await getPathKey(network, Number(i), prefix)).address;
const path = (await getPathKey(network, Number(i))).path;
const hdPath = getHDPath(network, path);
const { address, path, pubKey } = await getPathKey(
namespaceChainId,
Number(i),
);
const account: Account = {
counterId: Number(i),
pubKey: pubKey,
address: address,
hdPath: hdPath,
index: Number(i),
pubKey,
address,
hdPath: path,
};
return account;
}),
@ -182,77 +184,57 @@ export const retrieveAccountsForNetwork = async (
};
const retrieveAccounts = async (
prefix: string = 'cosmos',
): 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;
currentNetworkData: NetworksDataState,
): Promise<Account[] | undefined> => {
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
);
const accountIndices = accountIndicesServer && accountIndicesServer.password;
const ethLoadedAccounts = ethCounter
? await retrieveAccountsForNetwork('eth', ethCounter)
: undefined;
const cosmosLoadedAccounts = cosmosCounter
? await retrieveAccountsForNetwork('cosmos', cosmosCounter, prefix)
const loadedAccounts = accountIndices
? await retrieveAccountsForNetwork(
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
accountIndices,
)
: undefined;
return { ethLoadedAccounts, cosmosLoadedAccounts };
return loadedAccounts;
};
const retrieveSingleAccount = async (
network: string,
namespace: string,
chainId: string,
address: string,
prefix: string = 'cosmos',
) => {
let loadedAccounts;
switch (network) {
case 'eth':
const ethServer = await getInternetCredentials('eth:globalCounter');
const ethCounter = ethServer && ethServer.password;
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${namespace}:${chainId}`,
);
const accountIndices = accountIndicesServer && accountIndicesServer.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,
prefix,
);
}
break;
default:
break;
if (!accountIndices) {
throw new Error('Indices for given chain not found');
}
if (loadedAccounts) {
return loadedAccounts.find(account => account.address === address);
loadedAccounts = await retrieveAccountsForNetwork(
`${namespace}:${chainId}`,
accountIndices,
);
if (!loadedAccounts) {
throw new Error('Accounts for given chain not found');
}
return undefined;
return loadedAccounts.find(account => account.address === address);
};
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'),
resetKeyServers(EIP155),
resetKeyServers(COSMOS),
]);
} catch (error) {
console.error('Error resetting wallet:', error);
@ -262,11 +244,10 @@ const resetWallet = async () => {
const accountInfoFromHDPath = async (
hdPath: string,
prefix: string = COSMOS,
): Promise<
| { privKey: string; pubKey: string; address: string; network: string }
| undefined
{ privKey: string; pubKey: string; address: string } | undefined
> => {
// TODO: move HDNode inside eth switch case
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');
@ -280,62 +261,65 @@ const accountInfoFromHDPath = async (
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;
address = (await getCosmosAccounts(mnemonic, hdPath, prefix)).data
.address;
break;
default:
throw new Error('Invalid wallet type');
}
return { privKey, pubKey, address, network };
return { privKey, pubKey, address };
};
const getNextAccountId = async (network: string): Promise<number> => {
const idStore = await getInternetCredentials(`${network}:accountIndices`);
const getNextAccountId = async (namespaceChainId: string): Promise<number> => {
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
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 accountCounter = idStore.password;
const nextCounter = Number(accountCounter);
return nextCounter;
};
const updateAccountIndices = async (
network: string,
const updateAccountCounter = async (
namespaceChainId: string,
id: number,
): Promise<void> => {
const idStore = await getInternetCredentials(`${network}:accountIndices`);
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
if (!idStore) {
throw new Error('Account id not found');
}
const updatedIndices = `${idStore.password},${id.toString()}`;
await resetInternetCredentials(`${network}:accountIndices`);
const updatedCounter = String(id + 1);
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await setInternetCredentials(
`${network}:accountIndices`,
`${network}Counter`,
updatedIndices,
`addAccountCounter/${namespaceChainId}`,
'_',
updatedCounter,
);
};
const getCosmosAccounts = async (
mnemonic: string,
path: string,
prefix: string = COSMOS,
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [stringToPath(`m/44'/118'/${path}`)],
hdPaths: [stringToPath(path)],
prefix,
});
const accountsData = await cosmosWallet.getAccounts();
@ -355,6 +339,6 @@ export {
resetWallet,
accountInfoFromHDPath,
getNextAccountId,
updateAccountIndices,
updateAccountCounter,
getCosmosAccounts,
};

View File

@ -1,19 +1,22 @@
import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData';
import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
export const DEFAULTNETWORKS = [
export const EIP155 = 'eip155';
export const COSMOS = 'cosmos';
export const DEFAULT_NETWORKS = [
{
chainId: 'eip155:1',
chainId: '1',
networkName: EIP155_CHAINS['eip155:1'].name,
networkType: 'eth',
namespace: EIP155,
rpcUrl: EIP155_CHAINS['eip155:1'].rpc,
currencySymbol: 'ETH',
coinType: '60',
isDefault: true,
},
{
chainId: 'cosmos:theta-testnet-001',
chainId: 'theta-testnet-001',
networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name,
networkType: 'cosmos',
namespace: COSMOS,
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].rpc,
nativeDenom: 'uatom',
addressPrefix: 'cosmos',

View File

@ -10,9 +10,11 @@ import {
setInternetCredentials,
} from 'react-native-keychain';
import { AccountData, Secp256k1Wallet } from '@cosmjs/amino';
import { AccountData } from '@cosmjs/amino';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
import { EIP155 } from './constants';
import { NetworksDataState } from '../types';
const getMnemonic = async (): Promise<string> => {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
@ -24,8 +26,9 @@ const getMnemonic = async (): Promise<string> => {
return mnemonic;
};
const getHDPath = (network: string, path: string): string => {
return network === 'eth' ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
const getHDPath = (namespaceChainId: string, path: string): string => {
const namespace = namespaceChainId.split(':')[0];
return namespace === EIP155 ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
};
export const getDirectWallet = async (
@ -42,9 +45,8 @@ export const getDirectWallet = async (
};
const getPathKey = async (
network: string,
namespaceChainId: string,
accountId: number,
prefix: string = 'cosmos',
): Promise<{
path: string;
privKey: string;
@ -52,7 +54,7 @@ const getPathKey = async (
address: string;
}> => {
const pathKeyStore = await getInternetCredentials(
`${network}:keyServer:${accountId}`,
`accounts/${namespaceChainId}/${accountId}`,
);
if (!pathKeyStore) {
@ -63,84 +65,94 @@ const getPathKey = async (
const pathkey = pathKeyVal.split(',');
const path = pathkey[0];
const privKey = pathkey[1];
let pubKey: string;
let address: string;
if (network === 'eth') {
pubKey = pathkey[2];
address = pathkey[3];
} else {
// TODO: Store pubkey and address for cosmos instead of deriving
const wallet = await Secp256k1Wallet.fromKey(
Uint8Array.from(Buffer.from(privKey.split('0x')[1], 'hex')),
prefix,
);
const currAccount = await wallet.getAccounts();
pubKey = '0x' + Buffer.from(currAccount[0].pubkey).toString('hex');
address = currAccount[0].address;
}
const pubKey = pathkey[2];
const address = pathkey[3];
return { path, privKey, pubKey, address };
};
const getGlobalCounter = async (
network: string,
const getAccountIndices = async (
namespaceChainId: string,
): Promise<{
accountCounter: string;
counterIds: number[];
counterId: number;
accountIndices: string;
indices: number[];
index: number;
}> => {
const counterStore = await getInternetCredentials(`${network}:globalCounter`);
const counterStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
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;
let accountIndices = counterStore.password;
const indices = accountIndices.split(',').map(Number);
const index = indices[indices.length - 1] + 1;
return { accountCounter, counterIds, counterId };
return { accountIndices, indices, index };
};
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()}`;
const updateAccountIndices = async (
namespaceChainId: string,
): Promise<{ accountIndices: string; index: number }> => {
const accountIndicesData = await getAccountIndices(namespaceChainId);
const accountIndices = accountIndicesData.accountIndices;
const index = accountIndicesData.index;
const updatedAccountIndices = `${accountIndices},${index.toString()}`;
await resetInternetCredentials(`${network}:globalCounter`);
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
await setInternetCredentials(
`${network}:globalCounter`,
`${network}Global`,
updatedAccountCounter,
`accountIndices/${namespaceChainId}`,
'_',
updatedAccountIndices,
);
return { accountCounter: updatedAccountCounter, counterId };
return { accountIndices: updatedAccountIndices, index };
};
const resetKeyServers = async (prefix: string) => {
const idStore = await getInternetCredentials(`${prefix}:accountIndices`);
if (!idStore) {
throw new Error('Account id not found.');
const resetKeyServers = async (namespace: string) => {
const networksServer = await getInternetCredentials('networks');
if (!networksServer) {
throw new Error('Networks not found.');
}
const accountIds = idStore.password;
const ids = accountIds.split(',').map(Number);
const id = ids[ids.length - 1];
const networksData: NetworksDataState[] = JSON.parse(networksServer.password);
const filteredNetworks = networksData.filter(
network => network.namespace === namespace,
);
for (let i = 0; i <= id; i++) {
await resetInternetCredentials(`${prefix}:keyServer:${i}`);
if (filteredNetworks.length === 0) {
throw new Error(`No networks found for namespace ${namespace}.`);
}
filteredNetworks.forEach(async network => {
const { chainId } = network;
const namespaceChainId = `${namespace}:${chainId}`;
const idStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
if (!idStore) {
throw new Error(`Account indices not found for ${namespaceChainId}.`);
}
const accountIds = idStore.password;
const ids = accountIds.split(',').map(Number);
const latestId = Math.max(...ids);
for (let i = 0; i <= latestId; i++) {
await resetInternetCredentials(`accounts/${namespaceChainId}/${i}`);
}
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
});
};
export {
getMnemonic,
getPathKey,
updateGlobalCounter,
updateAccountIndices,
getHDPath,
resetKeyServers,
};

View File

@ -10,19 +10,21 @@ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
import { SignMessageParams } from '../types';
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
import { getCosmosAccounts } from './accounts';
import { COSMOS, EIP155 } from './constants';
const signMessage = async ({
message,
network,
namespace,
chainId,
accountId,
}: SignMessageParams): Promise<string | undefined> => {
const path = (await getPathKey(network, accountId)).path;
const path = await getPathKey(`${namespace}:${chainId}`, accountId);
switch (network) {
case 'eth':
return await signEthMessage(message, accountId);
case 'cosmos':
return await signCosmosMessage(message, path);
switch (namespace) {
case EIP155:
return await signEthMessage(message, accountId, chainId);
case COSMOS:
return await signCosmosMessage(message, path.path);
default:
throw new Error('Invalid wallet type');
}
@ -31,9 +33,11 @@ const signMessage = async ({
const signEthMessage = async (
message: string,
accountId: number,
chainId: string,
): Promise<string | undefined> => {
try {
const privKey = (await getPathKey('eth', accountId)).privKey;
const privKey = (await getPathKey(`${EIP155}:${chainId}`, accountId))
.privKey;
const wallet = new Wallet(privKey);
const signature = await wallet.signMessage(message);
@ -83,12 +87,12 @@ const signCosmosMessage = async (
};
const signDirectMessage = async (
network: string,
namespaceChainId: string,
accountId: number,
signDoc: SignDoc,
): Promise<string | undefined> => {
try {
const path = (await getPathKey(network, accountId)).path;
const path = (await getPathKey(namespaceChainId, accountId)).path;
const mnemonic = await getMnemonic();
const { directWallet, data } = await getDirectWallet(mnemonic, path);

View File

@ -21,19 +21,21 @@ export async function approveWalletConnectRequest(
networksData: NetworksDataState[],
requestEvent: SignClientTypes.EventArguments['session_request'],
account: Account,
network: string,
namespace: string,
chainId: string,
message?: string,
provider?: providers.JsonRpcProvider | SigningStargateClient,
) {
const { params, id } = requestEvent;
const { request } = params;
const chainId = requestEvent.params.chainId;
const requestChainId = requestEvent.params.chainId;
const requestedChain = networksData.find(
networkData => networkData.chainId === chainId,
networkData => networkData.chainId === requestChainId.split(':')[1],
);
const path = (await getPathKey(network, account.counterId)).path;
const path = (await getPathKey(`${namespace}:${chainId}`, account.index))
.path;
const mnemonic = await getMnemonic();
const cosmosAccount = await getCosmosAccounts(mnemonic, path);
const address = account.address;
@ -44,7 +46,9 @@ export async function approveWalletConnectRequest(
throw new Error('JSON RPC provider not found');
}
const privKey = (await getPathKey('eth', account.counterId)).privKey;
const privKey = (
await getPathKey(`${namespace}:${chainId}`, account.index)
).privKey;
const wallet = new Wallet(privKey);
const sendTransaction = request.params[0];
@ -64,7 +68,11 @@ export async function approveWalletConnectRequest(
throw new Error('Message to be signed not found');
}
const ethSignature = await signEthMessage(message, account.counterId);
const ethSignature = await signEthMessage(
message,
account.index,
chainId,
);
return formatJsonRpcResult(id, ethSignature);
case 'cosmos_signDirect':
@ -78,8 +86,8 @@ export async function approveWalletConnectRequest(
);
const cosmosDirectSignature = await signDirectMessage(
network,
account.counterId,
`${namespace}:${chainId}`,
account.index,
{
...request.params.signDoc,
bodyBytes: bodyBytesArray,
@ -106,7 +114,10 @@ export async function approveWalletConnectRequest(
});
case 'cosmos_sendTokens':
const amount = coins(request.params[0].value, requestedChain!.chainId);
const amount = coins(
request.params[0].value,
requestedChain!.nativeDenom!,
);
const gasPrice = GasPrice.fromString(
request.params[0].gasPrice.toString(),
);