Add support to configure networks (#76)

* Add support to configure cosmos networks

* Fix intents functionality for configured networks

* Add address prefix while initializing stargate client

* Remove unnecessary functions

* Update style for add network page

* Handle review changes

* Fix eth accounts not showing up on configured eth chain
This commit is contained in:
shreerang6921 2024-04-03 19:07:30 +05:30 committed by Nabarun Gogoi
parent bdd1b58140
commit e4fe88939c
11 changed files with 270 additions and 111 deletions

View File

@ -1,30 +1,22 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { View } from 'react-native';
import { List } from 'react-native-paper';
import { NetworkDropdownProps } from '../types';
import { NetworkDropdownProps, NetworksDataState } from '../types';
import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext';
const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
const [expanded, setExpanded] = useState<boolean>(false);
const [selectedNetwork, setSelectedNetwork] = useState<string>('Ethereum');
const { networksData } = useAccounts();
const networks = useMemo(() => {
return networksData.map(network => {
return {
value: network.networkType,
chainId: network.chainId,
displayName: network.networkName,
};
});
}, [networksData]);
const [expanded, setExpanded] = useState<boolean>(false);
const [selectedNetwork, setSelectedNetwork] = useState<string>(
networksData[0].networkName,
);
const handleNetworkPress = (network: string, displayName: string) => {
updateNetwork(network);
setSelectedNetwork(displayName);
const handleNetworkPress = (networksData: NetworksDataState) => {
updateNetwork(networksData);
setSelectedNetwork(networksData.networkName);
setExpanded(false);
};
@ -34,13 +26,11 @@ const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
title={selectedNetwork}
expanded={expanded}
onPress={() => setExpanded(!expanded)}>
{networks.map(network => (
{networksData.map(networkData => (
<List.Item
key={network.chainId}
title={network.displayName}
onPress={() =>
handleNetworkPress(network.value, network.displayName)
}
key={networkData.chainId}
title={networkData.networkName}
onPress={() => handleNetworkPress(networkData)}
/>
))}
</List.Accordion>

View File

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Text, List } from 'react-native-paper';
import styles from '../styles/stylesheet';
const SelectNetworkType = ({
updateNetworkType,
}: {
updateNetworkType: (networkType: string) => void;
}) => {
const [expanded, setExpanded] = useState<boolean>(false);
const [selectedNetwork, setSelectedNetwork] = useState<string>('ETH');
const networks = ['ETH', 'COSMOS'];
const handleNetworkPress = (network: string) => {
setSelectedNetwork(network);
updateNetworkType(network.toLowerCase());
setExpanded(false);
};
return (
<View style={styles.networkDropdown}>
<Text
style={{
fontWeight: 'bold',
marginBottom: 10,
}}>
Select Network Type
</Text>
<List.Accordion
title={selectedNetwork}
expanded={expanded}
onPress={() => setExpanded(!expanded)}>
{networks.map(network => (
<List.Item
key={network}
title={network}
onPress={() => handleNetworkPress(network)}
/>
))}
</List.Accordion>
</View>
);
};
export { SelectNetworkType };

View File

@ -34,6 +34,7 @@ const AccountsProvider = ({ children }: { children: any }) => {
ethAccounts: [],
cosmosAccounts: [],
});
// TODO: Replace chainId values with testnet chainIds
const [networksData, setNetworksData] = useState<NetworksDataState[]>([
{
chainId: 'eip155:11155111',
@ -47,7 +48,9 @@ const AccountsProvider = ({ children }: { children: any }) => {
networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name,
networkType: 'cosmos',
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].rpc,
currencySymbol: 'ATOM',
nativeDenom: 'ATOM',
addressPrefix: 'cosmos',
coinType: '118',
},
]);
const [currentIndex, setCurrentIndex] = useState<number>(0);

View File

@ -1,13 +1,15 @@
import React from 'react';
import { View } from 'react-native';
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 { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import styles from '../styles/stylesheet';
import { NetworksDataState, StackParamsList } from '../types';
import { useAccounts } from '../context/AccountsContext';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { SelectNetworkType } from '../components/SelectNetworkType';
// TODO: Add validation to form inputs
const AddNetwork = () => {
@ -24,12 +26,28 @@ const AddNetwork = () => {
const { networksData, setNetworksData } = useAccounts();
const submit = (data: NetworksDataState) => {
setNetworksData([...networksData, data]);
navigation.navigate('Laconic');
const [networkType, setNetworkType] = useState<string>('eth');
// TODO: Update session when new network is added with updated addresses
const updateNetworkType = (newNetworkType: string) => {
setNetworkType(newNetworkType);
};
const submit = useCallback(
async (data: NetworksDataState) => {
const updatedData = {
...data,
networkType,
};
setNetworksData([...networksData, updatedData]);
navigation.navigate('Laconic');
},
[navigation, networkType, networksData, setNetworksData],
);
return (
<View style={styles.signPage}>
// TODO: get form data from json file
<ScrollView contentContainerStyle={styles.addNetwork}>
<Controller
control={control}
defaultValue=""
@ -81,23 +99,6 @@ const AddNetwork = () => {
</>
)}
/>
<Controller
control={control}
name="currencySymbol"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Currency Symbol"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
</>
)}
/>
<HelperText type="error">{errors.currencySymbol?.message}</HelperText>
<Controller
control={control}
defaultValue=""
@ -111,35 +112,99 @@ const AddNetwork = () => {
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">
{errors.blockExplorerUrl?.message}
</HelperText>
</>
)}
/>
<HelperText type="error">{errors.blockExplorerUrl?.message}</HelperText>
<Controller
control={control}
// TODO: Use state to toggle between 'eth' and 'cosmos'
defaultValue="eth"
name="networkType"
render={({ field: { onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
disabled
label="Block Explorer URL (Optional)"
onBlur={onBlur}
/>
</>
)}
/>
<HelperText type="error">{errors.blockExplorerUrl?.message}</HelperText>
<SelectNetworkType updateNetworkType={updateNetworkType} />
{networkType === 'eth' ? (
<Controller
control={control}
name="currencySymbol"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Currency Symbol"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">
{errors.currencySymbol?.message}
</HelperText>
</>
)}
/>
) : (
<>
<Controller
control={control}
name="nativeDenom"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Native Denom"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">
{errors.nativeDenom?.message}
</HelperText>
</>
)}
/>
<Controller
control={control}
name="addressPrefix"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Address Prefix"
onBlur={onBlur}
onChangeText={value => onChange(value)}
/>
<HelperText type="error">
{errors.addressPrefix?.message}
</HelperText>
</>
)}
/>
<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
mode="contained"
onPress={handleSubmit(submit)}
disabled={!isValid}>
Submit
</Button>
</View>
</ScrollView>
);
};

View File

@ -55,19 +55,19 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const [cosmosStargateClient, setCosmosStargateClient] =
useState<SigningStargateClient>();
const requestedChain = networksData.find(
networkData => networkData.chainId === chainId,
);
const provider = useMemo(() => {
if (network === 'eth') {
const currentChain = networksData.find(
networkData => networkData.chainId === chainId,
);
if (!currentChain) {
if (!requestedChain) {
throw new Error('Requested chain not supported');
}
return new providers.JsonRpcProvider(currentChain.rpcUrl);
return new providers.JsonRpcProvider(requestedChain.rpcUrl);
}
}, [chainId, network, networksData]);
}, [requestedChain, network]);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -77,6 +77,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const requestAccount = await retrieveSingleAccount(
network,
requestAddress,
requestedChain?.addressPrefix,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
@ -86,7 +87,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
setAccount(requestAccount);
setIsLoading(false);
},
[navigation, network],
[navigation, network, requestedChain],
);
const gasFees = useMemo(() => {
@ -199,6 +200,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedChain?.addressPrefix,
);
const client = await SigningStargateClient.connectWithSigner(
@ -210,7 +212,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
};
setClient();
}, [account, chainId, network]);
}, [account, requestedChain, chainId, network]);
return (
<>

View File

@ -15,7 +15,7 @@ import ResetWalletDialog from '../components/ResetWalletDialog';
import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext';
import { StackParamsList } from '../types';
import { NetworksDataState, StackParamsList } from '../types';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
const WCLogo = () => {
@ -35,6 +35,7 @@ const HomeScreen = () => {
setCurrentIndex,
networkType,
setNetworkType,
networksData,
} = useAccounts();
const { setActiveSessions } = useWalletConnect();
@ -59,6 +60,9 @@ const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
const [isWalletCreating, setIsWalletCreating] = useState<boolean>(false);
const [walletDialog, setWalletDialog] = useState<boolean>(false);
const [currentChainId, setCurrentChainId] = useState<string>(
networksData[0].chainId,
);
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
const [phrase, setPhrase] = useState('');
@ -103,8 +107,9 @@ const HomeScreen = () => {
setNetworkType('eth');
};
const updateNetwork = (newNetwork: string) => {
setNetworkType(newNetwork);
const updateNetwork = (networksData: NetworksDataState) => {
setNetworkType(networksData.networkType);
setCurrentChainId(networksData.chainId);
setCurrentIndex(0);
};
@ -114,18 +119,25 @@ const HomeScreen = () => {
useEffect(() => {
const fetchAccounts = async () => {
if (isAccountsFetched) {
if (!currentChainId) {
return;
}
const { ethLoadedAccounts, cosmosLoadedAccounts } =
await retrieveAccounts();
if (ethLoadedAccounts && cosmosLoadedAccounts) {
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);
}
@ -133,7 +145,7 @@ const HomeScreen = () => {
};
fetchAccounts();
}, [isAccountsFetched, setAccounts]);
}, [currentChainId, networksData, setAccounts]);
return (
<View style={styles.appContainer}>
@ -144,10 +156,7 @@ const HomeScreen = () => {
</View>
) : isWalletCreated ? (
<>
<NetworkDropdown
selectedNetwork={networkType}
updateNetwork={updateNetwork}
/>
<NetworkDropdown updateNetwork={updateNetwork} />
<View style={styles.accountComponent}>
<Accounts
network={networkType}

View File

@ -21,10 +21,13 @@ import {
} from '../utils/wallet-connect/WalletConnectRequests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { useAccounts } from '../context/AccountsContext';
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
const SignRequest = ({ route }: SignRequestProps) => {
const { networksData } = useAccounts();
const requestSession = route.params.requestSessionData;
const requestName = requestSession?.peer?.metadata?.name;
const requestIcon = requestSession?.peer?.metadata?.icons[0];
@ -35,6 +38,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
const [network, setNetwork] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -68,9 +72,16 @@ const SignRequest = ({ route }: SignRequestProps) => {
requestAddress: string,
requestMessage: string,
) => {
const currentChain = networksData.find(networkData => {
if (networkData.addressPrefix) {
return requestAddress.includes(networkData.addressPrefix);
}
});
const requestAccount = await retrieveSingleAccount(
requestNetwork,
requestAddress,
currentChain?.addressPrefix,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
@ -88,7 +99,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
}
setIsLoading(false);
},
[account, message, navigation, network],
[account, message, navigation, network, networksData],
);
const sanitizePath = useCallback(
@ -178,6 +189,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
};
const rejectRequestHandler = async () => {
setIsRejecting(true);
if (route.params?.requestEvent) {
const response = rejectWalletConnectRequest(route.params?.requestEvent);
const { topic } = route.params?.requestEvent;
@ -186,6 +198,8 @@ const SignRequest = ({ route }: SignRequestProps) => {
response,
});
}
setIsRejecting(false);
navigation.navigate('Laconic');
};
@ -263,6 +277,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
<Button
mode="contained"
onPress={rejectRequestHandler}
loading={isRejecting}
buttonColor="#B82B0D">
No
</Button>

View File

@ -48,6 +48,10 @@ const styles = StyleSheet.create({
signPage: {
paddingHorizontal: 24,
},
addNetwork: {
paddingHorizontal: 24,
marginTop: 30,
},
accountInfo: {
marginTop: 12,
marginBottom: 30,

View File

@ -45,12 +45,7 @@ export type AccountsProps = {
};
export type NetworkDropdownProps = {
selectedNetwork: string;
updateNetwork: (network: string) => void;
customNetwork?: {
value: string;
displayName: string;
};
updateNetwork: (networksData: NetworksDataState) => void;
};
export type AccountsState = {
@ -62,9 +57,12 @@ export type NetworksDataState = {
networkName: string;
rpcUrl: string;
chainId: string;
currencySymbol: string;
currencySymbol?: string;
blockExplorerUrl?: string;
networkType: string;
nativeDenom?: string;
addressPrefix?: string;
coinType?: string;
};
export type SignMessageParams = {

View File

@ -124,16 +124,17 @@ const addAccountFromHDPath = async (
}
};
const retrieveAccountsForNetwork = async (
export const retrieveAccountsForNetwork = async (
network: string,
count: string,
accountsIndices: string,
prefix: string = 'cosmos',
): Promise<Account[]> => {
const elementsArray = count.split(',');
const accountsIndexArray = accountsIndices.split(',');
const loadedAccounts = await Promise.all(
elementsArray.map(async i => {
const pubKey = (await getPathKey(network, Number(i))).pubKey;
const address = (await getPathKey(network, Number(i))).address;
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);
@ -150,7 +151,9 @@ const retrieveAccountsForNetwork = async (
return loadedAccounts;
};
const retrieveAccounts = async (): Promise<{
const retrieveAccounts = async (
prefix: string = 'cosmos',
): Promise<{
ethLoadedAccounts?: Account[];
cosmosLoadedAccounts?: Account[];
}> => {
@ -163,13 +166,17 @@ const retrieveAccounts = async (): Promise<{
? await retrieveAccountsForNetwork('eth', ethCounter)
: undefined;
const cosmosLoadedAccounts = cosmosCounter
? await retrieveAccountsForNetwork('cosmos', cosmosCounter)
? await retrieveAccountsForNetwork('cosmos', cosmosCounter, prefix)
: undefined;
return { ethLoadedAccounts, cosmosLoadedAccounts };
};
const retrieveSingleAccount = async (network: string, address: string) => {
const retrieveSingleAccount = async (
network: string,
address: string,
prefix: string = 'cosmos',
) => {
let loadedAccounts;
switch (network) {
@ -190,6 +197,7 @@ const retrieveSingleAccount = async (network: string, address: string) => {
loadedAccounts = await retrieveAccountsForNetwork(
network,
cosmosCounter,
prefix,
);
}
break;
@ -228,6 +236,7 @@ const accountInfoFromHDPath = async (
| { privKey: string; pubKey: string; address: string; network: string }
| undefined
> => {
// TODO: move HDNode inside eth switch case
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');

View File

@ -10,7 +10,7 @@ import {
setInternetCredentials,
} from 'react-native-keychain';
import { AccountData } from '@cosmjs/amino';
import { AccountData, Secp256k1Wallet } from '@cosmjs/amino';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
@ -44,6 +44,7 @@ export const getDirectWallet = async (
const getPathKey = async (
network: string,
accountId: number,
prefix: string = 'cosmos',
): Promise<{
path: string;
privKey: string;
@ -62,8 +63,23 @@ const getPathKey = async (
const pathkey = pathKeyVal.split(',');
const path = pathkey[0];
const privKey = pathkey[1];
const pubKey = pathkey[2];
const address = pathkey[3];
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;
}
return { path, privKey, pubKey, address };
};