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 { getSignParamsMessage } from './utils/wallet-connect/Helpers';
import ApproveTransaction from './screens/ApproveTransaction'; import ApproveTransaction from './screens/ApproveTransaction';
import AddNetwork from './screens/AddNetwork'; import AddNetwork from './screens/AddNetwork';
import { COSMOS, EIP155 } from './utils/constants';
const Stack = createNativeStackNavigator<StackParamsList>(); const Stack = createNativeStackNavigator<StackParamsList>();
@ -44,7 +45,7 @@ const App = (): React.JSX.Element => {
const onSessionProposal = useCallback( const onSessionProposal = useCallback(
async (proposal: SignClientTypes.EventArguments['session_proposal']) => { async (proposal: SignClientTypes.EventArguments['session_proposal']) => {
if (!accounts.ethAccounts.length || !accounts.cosmosAccounts.length) { if (!accounts.length || !accounts.length) {
const { id } = proposal; const { id } = proposal;
await web3wallet!.rejectSession({ await web3wallet!.rejectSession({
id, id,
@ -55,7 +56,7 @@ const App = (): React.JSX.Element => {
setModalVisible(true); setModalVisible(true);
setCurrentProposal(proposal); setCurrentProposal(proposal);
}, },
[accounts.ethAccounts, accounts.cosmosAccounts], [accounts],
); );
const onSessionRequest = useCallback( const onSessionRequest = useCallback(
@ -68,7 +69,6 @@ const App = (): React.JSX.Element => {
switch (request.method) { switch (request.method) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION: case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
navigation.navigate('ApproveTransaction', { navigation.navigate('ApproveTransaction', {
network: 'eth',
transaction: request.params[0], transaction: request.params[0],
requestEvent, requestEvent,
requestSessionData, requestSessionData,
@ -77,7 +77,7 @@ const App = (): React.JSX.Element => {
case EIP155_SIGNING_METHODS.PERSONAL_SIGN: case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
navigation.navigate('SignRequest', { navigation.navigate('SignRequest', {
network: 'eth', namespace: EIP155,
address: request.params[1], address: request.params[1],
message: getSignParamsMessage(request.params), message: getSignParamsMessage(request.params),
requestEvent, requestEvent,
@ -103,7 +103,7 @@ const App = (): React.JSX.Element => {
), ),
}; };
navigation.navigate('SignRequest', { navigation.navigate('SignRequest', {
network: 'cosmos', namespace: COSMOS,
address: request.params.signerAddress, address: request.params.signerAddress,
message: JSON.stringify(message, undefined, 2), message: JSON.stringify(message, undefined, 2),
requestEvent, requestEvent,
@ -113,7 +113,7 @@ const App = (): React.JSX.Element => {
break; break;
case 'cosmos_signAmino': case 'cosmos_signAmino':
navigation.navigate('SignRequest', { navigation.navigate('SignRequest', {
network: 'cosmos', namespace: COSMOS,
address: request.params.signerAddress, address: request.params.signerAddress,
message: request.params.signDoc.memo, message: request.params.signDoc.memo,
requestEvent, requestEvent,
@ -124,7 +124,6 @@ const App = (): React.JSX.Element => {
case 'cosmos_sendTokens': case 'cosmos_sendTokens':
navigation.navigate('ApproveTransaction', { navigation.navigate('ApproveTransaction', {
network: 'cosmos',
transaction: request.params[0], transaction: request.params[0],
requestEvent, requestEvent,
requestSessionData, requestSessionData,
@ -173,7 +172,6 @@ const App = (): React.JSX.Element => {
options={{ options={{
headerTitle: () => <Text variant="titleLarge">Sign Message</Text>, headerTitle: () => <Text variant="titleLarge">Sign Message</Text>,
}} }}
initialParams={{ selectedNetwork: 'Ethereum' }}
/> />
<Stack.Screen <Stack.Screen
name="SignRequest" 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 { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData'; import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
const Accounts = ({ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
network,
currentIndex,
updateIndex: updateId,
}: AccountsProps) => {
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { accounts, setAccounts } = useAccounts(); const { accounts, setAccounts } = useAccounts();
const { networksData, currentChainId } = useNetworks(); const { selectedNetwork } = useNetworks();
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);
@ -36,22 +33,7 @@ const Accounts = ({
const handlePress = () => setExpanded(!expanded); const handlePress = () => setExpanded(!expanded);
const updateAccounts = (account: Account) => { const updateAccounts = (account: Account) => {
switch (network) { setAccounts([...accounts, account]);
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!');
}
}; };
useEffect(() => { useEffect(() => {
@ -70,24 +52,22 @@ const Accounts = ({
: undefined, : undefined,
); );
const currentNetwork = networksData.find( const namespaceChainId = `${selectedNetwork?.namespace}:${selectedNetwork?.chainId}`;
networkData => networkData.chainId === currentChainId,
);
let updatedNamespaces; let updatedNamespaces;
switch (currentNetwork?.networkType) { switch (selectedNetwork?.namespace) {
case 'eth': case EIP155:
updatedNamespaces = { updatedNamespaces = {
eip155: { eip155: {
chains: [currentNetwork.chainId], chains: [namespaceChainId],
// TODO: Debug optional namespace methods and events being required for approval // TODO: Debug optional namespace methods and events being required for approval
methods: [ methods: [
...Object.values(EIP155_SIGNING_METHODS), ...Object.values(EIP155_SIGNING_METHODS),
...(combinedNamespaces.eip155?.methods ?? []), ...(combinedNamespaces.eip155?.methods ?? []),
], ],
events: [...(combinedNamespaces.eip155?.events ?? [])], events: [...(combinedNamespaces.eip155?.events ?? [])],
accounts: accounts.ethAccounts.map(ethAccount => { accounts: accounts.map(ethAccount => {
return `${currentChainId}:${ethAccount.address}`; return `${namespaceChainId}:${ethAccount.address}`;
}), }),
}, },
cosmos: { cosmos: {
@ -98,17 +78,17 @@ const Accounts = ({
}, },
}; };
break; break;
case 'cosmos': case COSMOS:
updatedNamespaces = { updatedNamespaces = {
cosmos: { cosmos: {
chains: [currentNetwork.chainId], chains: [namespaceChainId],
methods: [ methods: [
...Object.values(COSMOS_METHODS), ...Object.values(COSMOS_METHODS),
...(combinedNamespaces.cosmos?.methods ?? []), ...(combinedNamespaces.cosmos?.methods ?? []),
], ],
events: [...(combinedNamespaces.cosmos?.events ?? [])], events: [...(combinedNamespaces.cosmos?.events ?? [])],
accounts: accounts.cosmosAccounts.map(cosmosAccount => { accounts: accounts.map(cosmosAccount => {
return `${currentChainId}:${cosmosAccount.address}`; return `${namespaceChainId}:${cosmosAccount.address}`;
}), }),
}, },
eip155: { eip155: {
@ -140,32 +120,21 @@ const Accounts = ({
const addAccountHandler = async () => { const addAccountHandler = async () => {
setIsAccountCreating(true); setIsAccountCreating(true);
const newAccount = await addAccount(network); const newAccount = await addAccount(selectedNetwork!);
setIsAccountCreating(false); setIsAccountCreating(false);
if (newAccount) { if (newAccount) {
updateAccounts(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 = () => const renderAccountItems = () =>
selectedAccounts.map(account => ( accounts.map(account => (
<List.Item <List.Item
key={account.counterId} key={account.index}
title={`Account ${account.counterId + 1}`} title={`Account ${account.index + 1}`}
onPress={() => { onPress={() => {
updateId(account.counterId); updateIndex(account.index);
setExpanded(false); setExpanded(false);
}} }}
/> />
@ -178,7 +147,7 @@ const Accounts = ({
visible={hdDialog} visible={hdDialog}
hideDialog={() => setHdDialog(false)} hideDialog={() => setHdDialog(false)}
updateAccounts={updateAccounts} updateAccounts={updateAccounts}
updateIndex={updateId} updateIndex={updateIndex}
pathCode={pathCode} pathCode={pathCode}
/> />
<List.Accordion <List.Accordion
@ -202,20 +171,26 @@ const Accounts = ({
mode="contained" mode="contained"
onPress={() => { onPress={() => {
setHdDialog(true); 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 Add Account from HD path
</Button> </Button>
</View> </View>
<AccountDetails account={selectedAccounts[currentIndex]} /> <AccountDetails account={accounts[currentIndex]} />
<View style={styles.signLink}> <View style={styles.signLink}>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
navigation.navigate('SignMessage', { navigation.navigate('SignMessage', {
selectedNetwork: network, selectedNamespace: selectedNetwork!.namespace,
accountInfo: selectedAccounts[currentIndex], selectedChainId: selectedNetwork!.chainId,
accountInfo: accounts[currentIndex],
}); });
}}> }}>
<Text <Text

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,22 @@ import React, { useCallback, useState } from 'react';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { TextInput, Button, HelperText } from 'react-native-paper'; 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 { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { NetworksDataState, StackParamsList } from '../types'; import { NetworksDataState, NetworksFormData, StackParamsList } from '../types';
import { SelectNetworkType } from '../components/SelectNetworkType'; import { SelectNetworkType } from '../components/SelectNetworkType';
import { storeNetworkData } from '../utils/accounts'; import { storeNetworkData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
import { getCosmosAccounts } from '../utils/accounts';
// TODO: Add validation to form inputs // TODO: Add validation to form inputs
const AddNetwork = () => { const AddNetwork = () => {
@ -25,29 +32,79 @@ const AddNetwork = () => {
mode: 'onChange', 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) => { const updateNetworkType = (newNetworkType: string) => {
setNetworkType(newNetworkType); setNamespace(newNetworkType);
}; };
const submit = useCallback( const submit = useCallback(
async (data: NetworksDataState) => { async (data: NetworksFormData) => {
const namespace = networkType === 'eth' ? 'eip155:' : 'cosmos:';
const newNetworkData = { const newNetworkData = {
...data, ...data,
chainId: `${namespace}${data.chainId}`, namespace,
networkType,
isDefault: false,
}; };
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.navigate('Laconic');
}, },
[navigation, networkType, networksData, setNetworksData], [navigation, namespace, setNetworksData],
); );
return ( 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} /> <SelectNetworkType updateNetworkType={updateNetworkType} />
{networkType === 'eth' ? ( {namespace === EIP155 ? (
<Controller <Controller
control={control} control={control}
name="currencySymbol" 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 <Button

View File

@ -28,6 +28,7 @@ import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import DataBox from '../components/DataBox'; import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc'; import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
type SignRequestProps = NativeStackScreenProps< type SignRequestProps = NativeStackScreenProps<
StackParamsList, StackParamsList,
@ -41,7 +42,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const requestName = requestSession.peer.metadata.name; const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0]; const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url; const requestURL = requestSession.peer.metadata.url;
const network = route.params.network; // TODO: Remove and access namespace from requestEvent
const transaction = route.params.transaction; const transaction = route.params.transaction;
const requestEvent = route.params.requestEvent; const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId; const chainId = requestEvent.params.chainId;
@ -54,18 +55,53 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useState<SigningStargateClient>(); useState<SigningStargateClient>();
const requestedChain = networksData.find( 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(() => { const provider = useMemo(() => {
if (network === 'eth') { if (namespace === EIP155) {
if (!requestedChain) { if (!requestedChain) {
throw new Error('Requested chain not supported'); throw new Error('Requested chain not supported');
} }
return new providers.JsonRpcProvider(requestedChain.rpcUrl); return new providers.JsonRpcProvider(requestedChain.rpcUrl);
} }
}, [requestedChain, network]); }, [requestedChain, namespace]);
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -73,9 +109,9 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const retrieveData = useCallback( const retrieveData = useCallback(
async (requestAddress: string) => { async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount( const requestAccount = await retrieveSingleAccount(
network, requestedChain!.namespace,
requestedChain!.chainId,
requestAddress, requestAddress,
requestedChain?.addressPrefix,
); );
if (!requestAccount) { if (!requestAccount) {
navigation.navigate('InvalidPath'); navigation.navigate('InvalidPath');
@ -85,11 +121,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
setAccount(requestAccount); setAccount(requestAccount);
setIsLoading(false); setIsLoading(false);
}, },
[navigation, network, requestedChain], [navigation, requestedChain],
); );
const gasFees = useMemo(() => { const gasFees = useMemo(() => {
if (network === 'eth') { if (namespace === EIP155) {
return BigNumber.from(transaction.gasLimit) return BigNumber.from(transaction.gasLimit)
.mul(BigNumber.from(transaction.gasPrice)) .mul(BigNumber.from(transaction.gasPrice))
.toString(); .toString();
@ -99,7 +135,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
return cosmosFees.amount[0].amount; return cosmosFees.amount[0].amount;
} }
}, [transaction, network]); }, [transaction, namespace]);
useEffect(() => { useEffect(() => {
retrieveData(transaction.from!); retrieveData(transaction.from!);
@ -115,9 +151,10 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
networksData, networksData,
requestEvent, requestEvent,
account, account,
network, namespace!,
requestedChain!.chainId,
'', '',
network === 'eth' ? provider : cosmosStargateClient, namespace === EIP155 ? provider : cosmosStargateClient,
); );
const { topic } = requestEvent; const { topic } = requestEvent;
@ -140,7 +177,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useEffect(() => { useEffect(() => {
const getAccountBalance = async (account: Account) => { const getAccountBalance = async (account: Account) => {
if (network === 'eth') { if (namespace === EIP155) {
const fetchedBalance = const fetchedBalance =
provider && (await provider.getBalance(account.address)); provider && (await provider.getBalance(account.address));
@ -158,7 +195,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
if (account) { if (account) {
getAccountBalance(account); getAccountBalance(account);
} }
}, [account, provider, network, cosmosStargateClient, requestedChain]); }, [account, provider, namespace, cosmosStargateClient, requestedChain]);
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
@ -184,35 +221,6 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, route.name]); }, [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 ( return (
<> <>
{isLoading ? ( {isLoading ? (
@ -240,14 +248,12 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</View> </View>
<DataBox <DataBox
label={`Balance (${ label={`Balance (${
requestedChain!.networkType === 'eth' namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
? 'wei' })`}
: requestedChain!.nativeDenom
}`}
data={ data={
balance === '' || balance === undefined balance === '' || balance === undefined
? 'Loading balance...' ? 'Loading balance...'
: `${balance})` : `${balance}`
} }
/> />
{transaction && ( {transaction && (
@ -255,9 +261,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
<DataBox label="To" data={transaction.to!} /> <DataBox label="To" data={transaction.to!} />
<DataBox <DataBox
label={`Amount (${ label={`Amount (${
requestedChain!.networkType === 'eth' namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
? 'wei'
: requestedChain!.nativeDenom
})`} })`}
data={BigNumber.from( data={BigNumber.from(
transaction.value?.toString(), transaction.value?.toString(),
@ -265,13 +269,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
/> />
<DataBox <DataBox
label={`Gas Fees (${ label={`Gas Fees (${
requestedChain!.networkType === 'eth' namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
? 'wei'
: requestedChain!.nativeDenom
})`} })`}
data={gasFees!} data={gasFees!}
/> />
{network === 'eth' && ( {namespace === EIP155 && (
<DataBox label="Data" data={transaction.data!} /> <DataBox label="Data" data={transaction.data!} />
)} )}
</View> </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 { View, ActivityIndicator, Image } from 'react-native';
import { Button, Text } from 'react-native-paper'; import { Button, Text } from 'react-native-paper';
@ -29,22 +29,16 @@ const WCLogo = () => {
}; };
const HomeScreen = () => { const HomeScreen = () => {
const { const { accounts, setAccounts, currentIndex, setCurrentIndex } =
accounts, useAccounts();
setAccounts,
currentIndex,
setCurrentIndex,
networkType,
setNetworkType,
} = useAccounts();
const { networksData, currentChainId, setCurrentChainId } = useNetworks(); const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
const { setActiveSessions } = useWalletConnect(); const { setActiveSessions } = useWalletConnect();
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
useEffect(() => { useEffect(() => {
if (accounts.ethAccounts.length > 0) { if (accounts.length > 0) {
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<Button onPress={() => navigation.navigate('WalletConnect')}> <Button onPress={() => navigation.navigate('WalletConnect')}>
@ -69,16 +63,27 @@ const HomeScreen = () => {
const hideWalletDialog = () => setWalletDialog(false); const hideWalletDialog = () => setWalletDialog(false);
const hideResetDialog = () => setResetWalletDialog(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 () => { const createWalletHandler = async () => {
setIsWalletCreating(true); setIsWalletCreating(true);
const { mnemonic, ethAccounts, cosmosAccounts } = await createWallet(); const mnemonic = await createWallet(networksData);
if (ethAccounts && cosmosAccounts) { if (mnemonic) {
setAccounts({ fetchAccounts();
ethAccounts: [...accounts.ethAccounts, ethAccounts],
cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts],
});
setWalletDialog(true); setWalletDialog(true);
setIsWalletCreated(true);
setPhrase(mnemonic); setPhrase(mnemonic);
} }
}; };
@ -87,10 +92,7 @@ const HomeScreen = () => {
await resetWallet(); await resetWallet();
setIsWalletCreated(false); setIsWalletCreated(false);
setIsWalletCreating(false); setIsWalletCreating(false);
setAccounts({ setAccounts([]);
ethAccounts: [],
cosmosAccounts: [],
});
setCurrentIndex(0); setCurrentIndex(0);
const sessions = web3wallet!.getActiveSessions(); const sessions = web3wallet!.getActiveSessions();
@ -103,12 +105,10 @@ const HomeScreen = () => {
setActiveSessions({}); setActiveSessions({});
hideResetDialog(); hideResetDialog();
setNetworkType('eth');
}; };
const updateNetwork = (networksData: NetworksDataState) => { const updateNetwork = (networksData: NetworksDataState) => {
setNetworkType(networksData.networkType); setSelectedNetwork(networksData);
setCurrentChainId(networksData.chainId);
setCurrentIndex(0); setCurrentIndex(0);
}; };
@ -117,34 +117,8 @@ const HomeScreen = () => {
}; };
useEffect(() => { 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(); fetchAccounts();
}, [currentChainId, networksData, setAccounts]); }, [networksData, setAccounts, selectedNetwork, fetchAccounts]);
return ( return (
<View style={styles.appContainer}> <View style={styles.appContainer}>
@ -157,11 +131,7 @@ const HomeScreen = () => {
<> <>
<NetworkDropdown updateNetwork={updateNetwork} /> <NetworkDropdown updateNetwork={updateNetwork} />
<View style={styles.accountComponent}> <View style={styles.accountComponent}>
<Accounts <Accounts currentIndex={currentIndex} updateIndex={updateIndex} />
network={networkType}
currentIndex={currentIndex}
updateIndex={updateIndex}
/>
</View> </View>
<View style={styles.resetContainer}> <View style={styles.resetContainer}>
<Button <Button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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