From 15da99a8275aea48de489076e0fc7c9ade120926 Mon Sep 17 00:00:00 2001 From: shreerang6921 <68148922+shreerang6921@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:10:34 +0530 Subject: [PATCH] Send back namespaces object based on chains requested by dApp (#94) * Refactor pairing modal code * Refactor update session code * Reset state after session proposal is approved * Refactor approve and update session * Remove unused helper methods * Remove completed todos --- src/App.tsx | 2 +- src/components/Accounts.tsx | 76 ++-------- src/components/PairingModal.tsx | 106 ++++--------- src/screens/ApproveTransaction.tsx | 1 - src/utils/wallet-connect/EIP155Lib.ts | 53 ------- src/utils/wallet-connect/Helpers.ts | 120 --------------- src/utils/wallet-connect/helpers.ts | 208 ++++++++++++++++++++++++++ 7 files changed, 253 insertions(+), 313 deletions(-) delete mode 100644 src/utils/wallet-connect/EIP155Lib.ts delete mode 100644 src/utils/wallet-connect/Helpers.ts create mode 100644 src/utils/wallet-connect/helpers.ts diff --git a/src/App.tsx b/src/App.tsx index 95a0a82..031b301 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,7 +25,7 @@ import WalletConnect from './screens/WalletConnect'; import { StackParamsList } from './types'; import { web3wallet } from './utils/wallet-connect/WalletConnectUtils'; 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 AddNetwork from './screens/AddNetwork'; import { COSMOS, EIP155 } from './utils/constants'; diff --git a/src/components/Accounts.tsx b/src/components/Accounts.tsx index 051e0eb..f483b5b 100644 --- a/src/components/Accounts.tsx +++ b/src/components/Accounts.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { ScrollView, TouchableOpacity, View } from 'react-native'; import { Button, List, Text, useTheme } from 'react-native-paper'; -import mergeWith from 'lodash/mergeWith'; import { setInternetCredentials } from 'react-native-keychain'; import { useNavigation } from '@react-navigation/native'; @@ -14,12 +13,10 @@ import HDPathDialog from './HDPathDialog'; import AccountDetails from './AccountDetails'; import { useAccounts } from '../context/AccountsContext'; import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils'; -import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data'; -import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData'; import { useNetworks } from '../context/NetworksContext'; -import { COSMOS, EIP155 } from '../utils/constants'; -import { NETWORK_METHODS } from '../utils/wallet-connect/common-data'; +import { EIP155 } from '../utils/constants'; import ConfirmDialog from './ConfirmDialog'; +import { getNamespaces } from '../utils/wallet-connect/helpers'; const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => { const navigation = @@ -52,68 +49,17 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => { for (const topic in sessions) { const session = sessions[topic]; - const combinedNamespaces = mergeWith( - session.requiredNamespaces, - session.optionalNamespaces, - (obj, src) => - Array.isArray(obj) && Array.isArray(src) - ? [...src, ...obj] - : undefined, + const { optionalNamespaces, requiredNamespaces } = session; + + const updatedNamespaces = await getNamespaces( + optionalNamespaces, + requiredNamespaces, + networksData, + selectedNetwork, + accounts, + currentIndex, ); - const namespaceChainId = `${selectedNetwork?.namespace}:${selectedNetwork?.chainId}`; - - let updatedNamespaces; - switch (selectedNetwork?.namespace) { - case EIP155: - updatedNamespaces = { - eip155: { - chains: [namespaceChainId], - // TODO: Debug optional namespace methods and events being required for approval - methods: [ - ...Object.values(EIP155_SIGNING_METHODS), - ...Object.values(NETWORK_METHODS), - ...(combinedNamespaces.eip155?.methods ?? []), - ], - events: [...(combinedNamespaces.eip155?.events ?? [])], - accounts: accounts.map(ethAccount => { - return `${namespaceChainId}:${ethAccount.address}`; - }), - }, - cosmos: { - chains: [], - methods: [], - events: [], - accounts: [], - }, - }; - break; - case COSMOS: - updatedNamespaces = { - cosmos: { - chains: [namespaceChainId], - methods: [ - ...Object.values(COSMOS_METHODS), - ...Object.values(NETWORK_METHODS), - ...(combinedNamespaces.cosmos?.methods ?? []), - ], - events: [...(combinedNamespaces.cosmos?.events ?? [])], - accounts: accounts.map(cosmosAccount => { - return `${namespaceChainId}:${cosmosAccount.address}`; - }), - }, - eip155: { - chains: [], - methods: [], - events: [], - accounts: [], - }, - }; - break; - default: - break; - } - if (!updatedNamespaces) { return; } diff --git a/src/components/PairingModal.tsx b/src/components/PairingModal.tsx index 45daa41..ae09041 100644 --- a/src/components/PairingModal.tsx +++ b/src/components/PairingModal.tsx @@ -11,11 +11,8 @@ import styles from '../styles/stylesheet'; import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils'; import { useAccounts } from '../context/AccountsContext'; import { useWalletConnect } from '../context/WalletConnectContext'; -import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data'; -import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData'; import { useNetworks } from '../context/NetworksContext'; -import { COSMOS, EIP155 } from '../utils/constants'; -import { NETWORK_METHODS } from '../utils/wallet-connect/common-data'; +import { getNamespaces } from '../utils/wallet-connect/helpers'; const PairingModal = ({ visible, @@ -25,7 +22,7 @@ const PairingModal = ({ setToastVisible, }: PairingModalProps) => { const { accounts, currentIndex } = useAccounts(); - const { selectedNetwork } = useNetworks(); + const { selectedNetwork, networksData } = useNetworks(); const [isLoading, setIsLoading] = useState(false); const dappName = currentProposal?.params?.proposer?.metadata.name; @@ -42,6 +39,18 @@ const PairingModal = ({ walletConnectChains: [], }); + const [supportedNamespaces, setSupportedNamespaces] = useState< + Record< + string, + { + chains: string[]; + methods: string[]; + events: string[]; + accounts: string[]; + } + > + >(); + useEffect(() => { if (!currentProposal) { return; @@ -79,78 +88,28 @@ const PairingModal = ({ const { setActiveSessions } = useWalletConnect(); - const supportedNamespaces = useMemo(() => { - if (!currentProposal) { - return; - } + useEffect(() => { + const getSupportedNamespaces = async () => { + if (!currentProposal) { + return; + } - // Set selected account as the first account in supported namespaces - const sortedAccounts = [ - accounts[currentIndex], - ...accounts.filter((account, index) => index !== currentIndex), - ]; + const { optionalNamespaces, requiredNamespaces } = currentProposal.params; - const namespaceChainId = `${selectedNetwork?.namespace}:${selectedNetwork?.chainId}`; + const nameSpaces = await getNamespaces( + optionalNamespaces, + requiredNamespaces, + networksData, + selectedNetwork, + accounts, + currentIndex, + ); - const { optionalNamespaces, requiredNamespaces } = currentProposal.params; + setSupportedNamespaces(nameSpaces); + }; - // TODO: Check with other dApps - switch (selectedNetwork?.namespace) { - case EIP155: - return { - eip155: { - chains: [namespaceChainId], - // TODO: Debug optional namespace methods and events being required for approval - methods: [ - ...Object.values(EIP155_SIGNING_METHODS), - ...Object.values(NETWORK_METHODS), - ...(optionalNamespaces.eip155?.methods ?? []), - ...(requiredNamespaces.eip155?.methods ?? []), - ], - events: [ - ...(optionalNamespaces.eip155?.events ?? []), - ...(requiredNamespaces.eip155?.events ?? []), - ], - accounts: sortedAccounts.map(ethAccount => { - return `${namespaceChainId}:${ethAccount.address}`; - }), - }, - cosmos: { - chains: [], - methods: [], - events: [], - accounts: [], - }, - }; - case COSMOS: - return { - cosmos: { - chains: [namespaceChainId], - methods: [ - ...Object.values(COSMOS_METHODS), - ...Object.values(NETWORK_METHODS), - ...(optionalNamespaces.cosmos?.methods ?? []), - ...(requiredNamespaces.cosmos?.methods ?? []), - ], - events: [ - ...(optionalNamespaces.cosmos?.events ?? []), - ...(requiredNamespaces.cosmos?.events ?? []), - ], - accounts: sortedAccounts.map(cosmosAccount => { - return `${namespaceChainId}:${cosmosAccount.address}`; - }), - }, - eip155: { - chains: [], - methods: [], - events: [], - accounts: [], - }, - }; - default: - break; - } - }, [accounts, currentProposal, currentIndex, selectedNetwork]); + getSupportedNamespaces(); + }, [currentProposal, networksData, selectedNetwork, accounts, currentIndex]); const namespaces = useMemo(() => { return ( @@ -180,6 +139,7 @@ const PairingModal = ({ setModalVisible(false); setToastVisible(true); setCurrentProposal(undefined); + setSupportedNamespaces(undefined); setWalletConnectData({ walletConnectMethods: [], walletConnectEvents: [], diff --git a/src/screens/ApproveTransaction.tsx b/src/screens/ApproveTransaction.tsx index acecc6f..d6412dd 100644 --- a/src/screens/ApproveTransaction.tsx +++ b/src/screens/ApproveTransaction.tsx @@ -42,7 +42,6 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { const requestName = requestSession.peer.metadata.name; const requestIcon = requestSession.peer.metadata.icons[0]; const requestURL = requestSession.peer.metadata.url; - // TODO: Remove and access namespace from requestEvent const transaction = route.params.transaction; const requestEvent = route.params.requestEvent; const chainId = requestEvent.params.chainId; diff --git a/src/utils/wallet-connect/EIP155Lib.ts b/src/utils/wallet-connect/EIP155Lib.ts deleted file mode 100644 index caeee02..0000000 --- a/src/utils/wallet-connect/EIP155Lib.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a - -import { providers, Wallet } from 'ethers'; - -/** - * Types - */ -interface IInitArgs { - mnemonic?: string; -} - -/** - * Library - */ -export default class EIP155Lib { - wallet: Wallet; - - constructor(wallet: Wallet) { - this.wallet = wallet; - } - - static init({ mnemonic }: IInitArgs) { - const wallet = mnemonic - ? Wallet.fromMnemonic(mnemonic) - : Wallet.createRandom(); - - return new EIP155Lib(wallet); - } - - getMnemonic() { - return this.wallet.mnemonic.phrase; - } - - getAddress() { - return this.wallet.address; - } - - signMessage(message: string) { - return this.wallet.signMessage(message); - } - - _signTypedData(domain: any, types: any, data: any) { - return this.wallet._signTypedData(domain, types, data); - } - - connect(provider: providers.JsonRpcProvider) { - return this.wallet.connect(provider); - } - - signTransaction(transaction: providers.TransactionRequest) { - return this.wallet.signTransaction(transaction); - } -} diff --git a/src/utils/wallet-connect/Helpers.ts b/src/utils/wallet-connect/Helpers.ts deleted file mode 100644 index 1098fa5..0000000 --- a/src/utils/wallet-connect/Helpers.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a - -import { utils } from 'ethers'; - -import { Account } from '../../types'; -import { EIP155_CHAINS, TEIP155Chain } from './EIP155Data'; - -/** - * Truncates string (in the middle) via given lenght value - */ -export function truncate(value: string, length: number) { - if (value?.length <= length) { - return value; - } - - const separator = '...'; - const stringLength = length - separator.length; - const frontLength = Math.ceil(stringLength / 2); - const backLength = Math.floor(stringLength / 2); - - return ( - value.substring(0, frontLength) + - separator + - value.substring(value.length - backLength) - ); -} - -/** - * Converts hex to utf8 string if it is valid bytes - */ -export function convertHexToUtf8(value: string) { - if (utils.isHexString(value)) { - return utils.toUtf8String(value); - } - - return value; -} - -/** - * Gets message from various signing request methods by filtering out - * a value that is not an address (thus is a message). - * If it is a hex string, it gets converted to utf8 string - */ -export function getSignParamsMessage(params: string[]) { - const message = params.filter(p => !utils.isAddress(p))[0]; - - return convertHexToUtf8(message); -} - -/** - * Gets data from various signTypedData request methods by filtering out - * a value that is not an address (thus is data). - * If data is a string convert it to object - */ -export function getSignTypedDataParamsData(params: string[]) { - const data = params.filter(p => !utils.isAddress(p))[0]; - - if (typeof data === 'string') { - return JSON.parse(data); - } - - return data; -} - -/** - * Get our address from params checking if params string contains one - * of our wallet addresses - */ -export async function getAccountNumberFromParams( - addresses: string[], - ethAccounts: Account[], - params: any, -) { - const paramsString = JSON.stringify(params); - let address = ''; - - addresses.forEach(addr => { - if (paramsString.includes(addr)) { - address = addr; - } - }); - - const currentAccount = ethAccounts!.find( - account => account.address === address, - ); - - if (!currentAccount) { - throw new Error('Account with given adress not found'); - } - - return currentAccount.counterId; -} - -/** - * Check if chain is part of EIP155 standard - */ -export function isEIP155Chain(chain: string) { - return chain.includes('eip155'); -} - -/** - * Check if chain is part of COSMOS standard - */ -export function isCosmosChain(chain: string) { - return chain.includes('cosmos'); -} - -/** - * Check if chain is part of SOLANA standard - */ -export function isSolanaChain(chain: string) { - return chain.includes('solana'); -} - -/** - * Formats chainId to its name - */ -export function formatChainName(chainId: string) { - return EIP155_CHAINS[chainId as TEIP155Chain]?.name ?? chainId; -} diff --git a/src/utils/wallet-connect/helpers.ts b/src/utils/wallet-connect/helpers.ts new file mode 100644 index 0000000..306ff64 --- /dev/null +++ b/src/utils/wallet-connect/helpers.ts @@ -0,0 +1,208 @@ +// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a + +import { utils } from 'ethers'; + +import { ProposalTypes } from '@walletconnect/types'; + +import { Account, NetworksDataState } from '../../types'; +import { EIP155_SIGNING_METHODS } from './EIP155Data'; +import { mergeWith } from 'lodash'; +import { retrieveAccounts } from '../accounts'; +import { COSMOS, EIP155 } from '../constants'; +import { NETWORK_METHODS } from './common-data'; +import { COSMOS_METHODS } from './COSMOSData'; + +/** + * Converts hex to utf8 string if it is valid bytes + */ +export function convertHexToUtf8(value: string) { + if (utils.isHexString(value)) { + return utils.toUtf8String(value); + } + + return value; +} + +/** + * Gets message from various signing request methods by filtering out + * a value that is not an address (thus is a message). + * If it is a hex string, it gets converted to utf8 string + */ +export function getSignParamsMessage(params: string[]) { + const message = params.filter(p => !utils.isAddress(p))[0]; + + return convertHexToUtf8(message); +} + +export const getNamespaces = async ( + optionalNamespaces: ProposalTypes.OptionalNamespaces, + requiredNamespaces: ProposalTypes.RequiredNamespaces, + networksData: NetworksDataState[], + selectedNetwork: NetworksDataState, + accounts: Account[], + currentIndex: number, +) => { + const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`; + + const combinedNamespaces = mergeWith( + requiredNamespaces, + optionalNamespaces, + (obj, src) => + Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined, + ); + + const walletConnectChains: string[] = []; + + Object.keys(combinedNamespaces).forEach(key => { + const { chains } = combinedNamespaces[key]; + + chains && walletConnectChains.push(...chains); + }); + + // If combinedNamespaces is not empty, send back namespaces object based on requested chains + // Else send back namespaces object using currently selected network + if (Object.keys(combinedNamespaces).length > 0) { + if (!(walletConnectChains.length > 0)) { + return; + } + + // Get required networks + const requiredNetworks = networksData.filter(network => + walletConnectChains.includes(`${network.namespace}:${network.chainId}`), + ); + // Get accounts for required networks + const requiredAddressesPromise = requiredNetworks.map( + async requiredNetwork => { + const retrievedAccounts = await retrieveAccounts(requiredNetwork); + + if (!retrievedAccounts) { + throw new Error('Accounts for given network not found'); + } + + const addresses = retrievedAccounts.map( + retrieveAccount => + `${requiredNetwork.namespace}:${requiredNetwork.chainId}:${retrieveAccount.address}`, + ); + + return addresses; + }, + ); + + const requiredAddressesArray = await Promise.all(requiredAddressesPromise); + const requiredAddresses = requiredAddressesArray.flat(); + + let sortedAccounts = requiredAddresses; + + // If selected network is included in chains requested from dApp, + // Put selected account as first account + if (walletConnectChains.includes(namespaceChainId)) { + const currentAddresses = requiredAddresses.filter(address => + address.includes(namespaceChainId), + ); + sortedAccounts = [ + currentAddresses[currentIndex], + ...currentAddresses.filter((address, index) => index !== currentIndex), + ...requiredAddresses.filter( + address => !currentAddresses.includes(address), + ), + ]; + } + + // construct namespace object + const newNamespaces = { + eip155: { + chains: walletConnectChains.filter(chain => chain.includes(EIP155)), + // TODO: Debug optional namespace methods and events being required for approval + methods: [ + ...Object.values(EIP155_SIGNING_METHODS), + ...Object.values(NETWORK_METHODS), + ...(optionalNamespaces.eip155?.methods ?? []), + ...(requiredNamespaces.eip155?.methods ?? []), + ], + events: [ + ...(optionalNamespaces.eip155?.events ?? []), + ...(requiredNamespaces.eip155?.events ?? []), + ], + accounts: sortedAccounts.filter(account => account.includes(EIP155)), + }, + cosmos: { + chains: walletConnectChains.filter(chain => chain.includes(COSMOS)), + methods: [ + ...Object.values(COSMOS_METHODS), + ...Object.values(NETWORK_METHODS), + ...(optionalNamespaces.cosmos?.methods ?? []), + ...(requiredNamespaces.cosmos?.methods ?? []), + ], + events: [ + ...(optionalNamespaces.cosmos?.events ?? []), + ...(requiredNamespaces.cosmos?.events ?? []), + ], + accounts: sortedAccounts.filter(account => account.includes(COSMOS)), + }, + }; + + return newNamespaces; + } else { + // Set selected account as the first account in supported namespaces + const sortedAccounts = [ + accounts[currentIndex], + ...accounts.filter((account, index) => index !== currentIndex), + ]; + + switch (selectedNetwork.namespace) { + case EIP155: + return { + eip155: { + chains: [namespaceChainId], + // TODO: Debug optional namespace methods and events being required for approval + methods: [ + ...Object.values(EIP155_SIGNING_METHODS), + ...Object.values(NETWORK_METHODS), + ...(optionalNamespaces.eip155?.methods ?? []), + ...(requiredNamespaces.eip155?.methods ?? []), + ], + events: [ + ...(optionalNamespaces.eip155?.events ?? []), + ...(requiredNamespaces.eip155?.events ?? []), + ], + accounts: sortedAccounts.map(ethAccount => { + return `${namespaceChainId}:${ethAccount.address}`; + }), + }, + cosmos: { + chains: [], + methods: [], + events: [], + accounts: [], + }, + }; + case COSMOS: + return { + cosmos: { + chains: [namespaceChainId], + methods: [ + ...Object.values(COSMOS_METHODS), + ...Object.values(NETWORK_METHODS), + ...(optionalNamespaces.cosmos?.methods ?? []), + ...(requiredNamespaces.cosmos?.methods ?? []), + ], + events: [ + ...(optionalNamespaces.cosmos?.events ?? []), + ...(requiredNamespaces.cosmos?.events ?? []), + ], + accounts: sortedAccounts.map(cosmosAccount => { + return `${namespaceChainId}:${cosmosAccount.address}`; + }), + }, + eip155: { + chains: [], + methods: [], + events: [], + accounts: [], + }, + }; + default: + break; + } + } +};