diff --git a/src/components/NetworkDropdown.tsx b/src/components/NetworkDropdown.tsx index 0abfee3..cc5d329 100644 --- a/src/components/NetworkDropdown.tsx +++ b/src/components/NetworkDropdown.tsx @@ -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(false); - const [selectedNetwork, setSelectedNetwork] = useState('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(false); + const [selectedNetwork, setSelectedNetwork] = useState( + 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 => ( - handleNetworkPress(network.value, network.displayName) - } + key={networkData.chainId} + title={networkData.networkName} + onPress={() => handleNetworkPress(networkData)} /> ))} diff --git a/src/components/SelectNetworkType.tsx b/src/components/SelectNetworkType.tsx new file mode 100644 index 0000000..cb05253 --- /dev/null +++ b/src/components/SelectNetworkType.tsx @@ -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(false); + const [selectedNetwork, setSelectedNetwork] = useState('ETH'); + + const networks = ['ETH', 'COSMOS']; + + const handleNetworkPress = (network: string) => { + setSelectedNetwork(network); + updateNetworkType(network.toLowerCase()); + setExpanded(false); + }; + + return ( + + + Select Network Type + + setExpanded(!expanded)}> + {networks.map(network => ( + handleNetworkPress(network)} + /> + ))} + + + ); +}; + +export { SelectNetworkType }; diff --git a/src/context/AccountsContext.tsx b/src/context/AccountsContext.tsx index 9e06d63..5314df9 100644 --- a/src/context/AccountsContext.tsx +++ b/src/context/AccountsContext.tsx @@ -34,6 +34,7 @@ const AccountsProvider = ({ children }: { children: any }) => { ethAccounts: [], cosmosAccounts: [], }); + // TODO: Replace chainId values with testnet chainIds const [networksData, setNetworksData] = useState([ { 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(0); diff --git a/src/screens/AddNetwork.tsx b/src/screens/AddNetwork.tsx index 56b41c3..439a6b0 100644 --- a/src/screens/AddNetwork.tsx +++ b/src/screens/AddNetwork.tsx @@ -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('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 ( - + // TODO: get form data from json file + { )} /> - ( - <> - onChange(value)} - /> - - )} - /> - {errors.currencySymbol?.message} { onBlur={onBlur} onChangeText={value => onChange(value)} /> + + {errors.blockExplorerUrl?.message} + )} /> - {errors.blockExplorerUrl?.message} - ( - <> - - - )} - /> - {errors.blockExplorerUrl?.message} + + {networkType === 'eth' ? ( + ( + <> + onChange(value)} + /> + + {errors.currencySymbol?.message} + + + )} + /> + ) : ( + <> + ( + <> + onChange(value)} + /> + + {errors.nativeDenom?.message} + + + )} + /> + ( + <> + onChange(value)} + /> + + {errors.addressPrefix?.message} + + + )} + /> + ( + <> + onChange(value)} + /> + {errors.coinType?.message} + + )} + /> + + )} - + ); }; diff --git a/src/screens/ApproveTransaction.tsx b/src/screens/ApproveTransaction.tsx index 66f478f..427a95e 100644 --- a/src/screens/ApproveTransaction.tsx +++ b/src/screens/ApproveTransaction.tsx @@ -55,19 +55,19 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { const [cosmosStargateClient, setCosmosStargateClient] = useState(); + 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>(); @@ -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 ( <> diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 1b81461..02e68c7 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -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(false); const [isWalletCreating, setIsWalletCreating] = useState(false); const [walletDialog, setWalletDialog] = useState(false); + const [currentChainId, setCurrentChainId] = useState( + networksData[0].chainId, + ); const [resetWalletDialog, setResetWalletDialog] = useState(false); const [isAccountsFetched, setIsAccountsFetched] = useState(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 ( @@ -144,10 +156,7 @@ const HomeScreen = () => { ) : isWalletCreated ? ( <> - + ; 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(''); const [isLoading, setIsLoading] = useState(true); const [isApproving, setIsApproving] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); const navigation = useNavigation>(); @@ -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) => { diff --git a/src/styles/stylesheet.js b/src/styles/stylesheet.js index eaf2084..d2fa756 100644 --- a/src/styles/stylesheet.js +++ b/src/styles/stylesheet.js @@ -48,6 +48,10 @@ const styles = StyleSheet.create({ signPage: { paddingHorizontal: 24, }, + addNetwork: { + paddingHorizontal: 24, + marginTop: 30, + }, accountInfo: { marginTop: 12, marginBottom: 30, diff --git a/src/types.ts b/src/types.ts index 87734ad..2f17e30 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 = { diff --git a/src/utils/accounts.ts b/src/utils/accounts.ts index 3d3213b..7bdf899 100644 --- a/src/utils/accounts.ts +++ b/src/utils/accounts.ts @@ -124,16 +124,17 @@ const addAccountFromHDPath = async ( } }; -const retrieveAccountsForNetwork = async ( +export const retrieveAccountsForNetwork = async ( network: string, - count: string, + accountsIndices: string, + prefix: string = 'cosmos', ): Promise => { - 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!'); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 801904f..c8cf406 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -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 }; };