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
This commit is contained in:
shreerang6921 2024-04-17 10:10:34 +05:30 committed by Nabarun Gogoi
parent 888302a0dd
commit 15da99a827
7 changed files with 253 additions and 313 deletions

View File

@ -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';

View File

@ -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;
}

View File

@ -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: [],

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}
};