Support SIWE using selected account (#61)

* Sign in using selected address

* Use optional namespace

* Merge namespaces and use combined namespace

* Add todo for lodash merge

* Use walletConnect util buildApprovedNamespaces while approving sessions

* Lint fixes

---------

Co-authored-by: Adw8 <adwait@deepstacksoft.com>
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
Adwait Gharpure 2024-03-28 11:10:55 +05:30 committed by GitHub
parent 3a2087b389
commit 4eecdb7e4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 255 additions and 273 deletions

View File

@ -89,7 +89,7 @@
5. Set up the Android device:
- For a physical device, refer to the [React Native documentation for running on a physical device]("https://reactnative.dev/docs/running-on-device)
- For a physical device, refer to the [React Native documentation for running on a physical device](https://reactnative.dev/docs/running-on-device)
- For a virtual device, continue with the steps.

View File

@ -7,7 +7,7 @@ import React from 'react';
import App from '../src/App';
// Note: import explicitly to use the types shipped with jest.
import {it} from '@jest/globals';
import { it } from '@jest/globals';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';

View File

@ -1,4 +1,4 @@
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
/**
* Metro configuration

View File

@ -26,6 +26,7 @@
"cosmjs-types": "^0.9.0",
"ethers": "5.7.2",
"fast-text-encoding": "^1.0.6",
"lodash": "^4.17.21",
"metro-react-native-babel-preset": "^0.77.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
@ -51,6 +52,7 @@
"@react-native/eslint-config": "0.73.2",
"@react-native/metro-config": "0.73.4",
"@react-native/typescript-config": "0.73.1",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.6",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^18.0.0",

View File

@ -23,7 +23,7 @@ import AddSession from './screens/AddSession';
import WalletConnect from './screens/WalletConnect';
import { StackParamsList } from './types';
import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Lib';
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
import { getSignParamsMessage } from './utils/wallet-connect/Helpers';
import ApproveTransaction from './screens/ApproveTransaction';

View File

@ -1,15 +1,17 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Image, View, Modal, ScrollView } from 'react-native';
import { Button, Text } from 'react-native-paper';
import mergeWith from 'lodash/mergeWith';
import { SessionTypes } from '@walletconnect/types';
import { getSdkError } from '@walletconnect/utils';
import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
import { PairingModalProps } from '../types';
import { AccountsState, PairingModalProps } from '../types';
import styles from '../styles/stylesheet';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext';
import { EIP155_CHAINS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_MAINNET_CHAINS } from '../utils/wallet-connect/COSMOSData';
const PairingModal = ({
visible,
@ -18,7 +20,7 @@ const PairingModal = ({
setModalVisible,
setToastVisible,
}: PairingModalProps) => {
const { accounts } = useAccounts();
const { accounts, currentIndex } = useAccounts();
const url = currentProposal?.params?.proposer?.metadata.url;
const icon = currentProposal?.params.proposer?.metadata.icons[0];
@ -38,126 +40,147 @@ const PairingModal = ({
return;
}
const { params } = currentProposal;
const { requiredNamespaces } = params;
const { requiredNamespaces, optionalNamespaces } = params;
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
Object.keys(requiredNamespaces).forEach(key => {
switch (key) {
case 'eip155':
const {
methods: ethMethods,
events: ethEvents,
chains: ethChains,
} = currentProposal?.params?.requiredNamespaces.eip155;
setWalletConnectData(prevData => {
return {
walletConnectMethods: [
...prevData.walletConnectMethods,
...ethMethods,
],
walletConnectEvents: [
...prevData.walletConnectEvents,
...ethEvents,
],
walletConnectChains: ethChains
? [...prevData.walletConnectChains, ...ethChains]
: [...prevData.walletConnectChains],
};
});
break;
case 'cosmos':
const {
methods: cosmosMethods,
events: cosmosEvents,
chains: cosmosChains,
} = currentProposal?.params?.requiredNamespaces.cosmos;
const combinedNamespaces = mergeWith(
requiredNamespaces,
optionalNamespaces,
(obj, src) =>
Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined,
);
Object.keys(combinedNamespaces).forEach(key => {
const { methods, events, chains } = combinedNamespaces[key];
setWalletConnectData(prevData => {
return {
walletConnectMethods: [
...prevData.walletConnectMethods,
...cosmosMethods,
],
walletConnectEvents: [
...prevData.walletConnectEvents,
...cosmosEvents,
],
walletConnectChains: cosmosChains
? [...prevData.walletConnectChains, ...cosmosChains]
: [...prevData.walletConnectChains],
};
});
break;
default:
throw new Error(`${key} not supported`);
}
setWalletConnectData(prevData => {
return {
walletConnectMethods: [...prevData.walletConnectMethods, ...methods],
walletConnectEvents: [...prevData.walletConnectEvents, ...events],
walletConnectChains: chains
? [...prevData.walletConnectChains, ...chains]
: [...prevData.walletConnectChains],
};
});
});
}, [currentProposal]);
const { setActiveSessions } = useWalletConnect();
const handleAccept = async () => {
if (currentProposal) {
const { id, params } = currentProposal;
const { requiredNamespaces, relays } = params;
const namespaces: SessionTypes.Namespaces = {};
const supportedNamespaces = useMemo(() => {
if (!currentProposal) {
return;
}
Object.keys(requiredNamespaces).forEach(key => {
let currentAddresses: string[];
// eip155
const eip155Chains = Object.keys(EIP155_CHAINS);
switch (key) {
case 'eip155':
if (accounts.ethAccounts.length > 0) {
currentAddresses = accounts.ethAccounts.map(
account => account.address,
);
}
break;
case 'cosmos':
if (accounts.cosmosAccounts.length > 0) {
currentAddresses = accounts.cosmosAccounts.map(
account => account.address,
);
}
break;
default:
throw new Error(`${key} not supported`);
// cosmos
const cosmosChains = Object.keys(COSMOS_MAINNET_CHAINS);
// Set selected account as the first account in supported namespaces
const sortedAccounts = Object.entries(accounts).reduce(
(acc: AccountsState, [key, value]) => {
let newValue = [...value];
// TODO: Implement selectedAccount instead of currentIndex in AccountsContext
if (value.length > currentIndex) {
const currentAccount = newValue[currentIndex];
const remainingAccounts = newValue.filter(
(_, index) => index !== currentIndex,
);
newValue = [currentAccount, ...remainingAccounts];
}
const namespaceAccounts: string[] = [];
requiredNamespaces[key].chains!.map((chain: string) => {
currentAddresses.map(acc =>
namespaceAccounts.push(`${chain}:${acc}`),
);
acc[key as 'ethAccounts' | 'cosmosAccounts'] = newValue;
return acc;
},
{ ethAccounts: [], cosmosAccounts: [] },
);
const { optionalNamespaces, requiredNamespaces } = currentProposal.params;
return {
eip155: {
chains: eip155Chains,
// TODO: Debug optional namespace methods and events being required for approval
methods: [
...(optionalNamespaces.eip155?.methods ?? []),
...(requiredNamespaces.eip155?.methods ?? []),
],
events: [
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: eip155Chains
.map(chain =>
sortedAccounts.ethAccounts.map(
account => `${chain}:${account.address}`,
),
)
.flat(),
},
cosmos: {
chains: cosmosChains,
methods: [
...(optionalNamespaces.cosmos?.methods ?? []),
...(requiredNamespaces.cosmos?.methods ?? []),
],
events: [
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: cosmosChains
.map(chain =>
sortedAccounts.cosmosAccounts.map(
account => `${chain}:${account.address}`,
),
)
.flat(),
},
};
}, [currentIndex, accounts, currentProposal]);
const namespaces = useMemo(() => {
return (
currentProposal &&
supportedNamespaces &&
buildApprovedNamespaces({
proposal: currentProposal.params,
supportedNamespaces,
})
);
}, [currentProposal, supportedNamespaces]);
const handleAccept = async () => {
try {
if (currentProposal && namespaces) {
const { id } = currentProposal;
await web3wallet!.approveSession({
id,
namespaces,
});
namespaces[key] = {
accounts: namespaceAccounts,
methods: requiredNamespaces[key].methods,
events: requiredNamespaces[key].events,
};
});
await web3wallet!.approveSession({
id,
relayProtocol: relays[0].protocol,
namespaces,
});
const sessions = web3wallet!.getActiveSessions();
setActiveSessions(sessions);
setModalVisible(false);
setToastVisible(true);
setCurrentProposal(undefined);
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
const sessions = web3wallet!.getActiveSessions();
setActiveSessions(sessions);
setModalVisible(false);
setToastVisible(true);
setCurrentProposal(undefined);
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
}
} catch (error) {
console.error('Error in approve session:', error);
throw error;
}
};
@ -216,11 +239,11 @@ const PairingModal = ({
</View>
<View style={styles.flexRow}>
<Button mode="contained" onPress={() => handleAccept()}>
<Button mode="contained" onPress={handleAccept}>
Accept
</Button>
<View style={styles.space} />
<Button mode="outlined" onPress={() => handleReject()}>
<Button mode="outlined" onPress={handleReject}>
Cancel
</Button>
</View>

View File

@ -5,9 +5,13 @@ import { AccountsState } from '../types';
const AccountsContext = createContext<{
accounts: AccountsState;
setAccounts: (account: AccountsState) => void;
currentIndex: number;
setCurrentIndex: (index: number) => void;
}>({
accounts: { ethAccounts: [], cosmosAccounts: [] },
setAccounts: () => {},
currentIndex: 0,
setCurrentIndex: () => {},
});
const useAccounts = () => {
@ -20,8 +24,10 @@ const AccountsProvider = ({ children }: { children: any }) => {
ethAccounts: [],
cosmosAccounts: [],
});
const [currentIndex, setCurrentIndex] = useState<number>(0);
return (
<AccountsContext.Provider value={{ accounts, setAccounts }}>
<AccountsContext.Provider
value={{ accounts, setAccounts, currentIndex, setCurrentIndex }}>
{children}
</AccountsContext.Provider>
);

View File

@ -9,7 +9,7 @@ export default function useInitialization() {
await createWeb3Wallet();
setInitialized(true);
} catch (err: unknown) {
console.log('Error for initializing', err);
console.error('Error for initializing', err);
}
}, []);

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Image, ScrollView, View } from 'react-native';
import { ActivityIndicator, Button, Text, Appbar } from 'react-native-paper';
import { PopulatedTransaction, providers, BigNumber } from 'ethers';
import { providers, BigNumber } from 'ethers';
import { useNavigation } from '@react-navigation/native';
import {
@ -61,16 +61,22 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const retrieveData = async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount(network, requestAddress);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
const retrieveData = useCallback(
async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount(
network,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
setAccount(requestAccount);
setIsLoading(false);
};
setAccount(requestAccount);
setIsLoading(false);
},
[navigation, network],
);
const gasFees = useMemo(() => {
if (network === 'eth') {
@ -87,7 +93,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useEffect(() => {
retrieveData(transaction.from!);
}, [route]);
}, [retrieveData, transaction]);
const acceptRequestHandler = async () => {
if (!account) {

View File

@ -28,7 +28,8 @@ const WCLogo = () => {
};
const HomeScreen = () => {
const { accounts, setAccounts } = useAccounts();
const { accounts, setAccounts, currentIndex, setCurrentIndex } =
useAccounts();
const { setActiveSessions } = useWalletConnect();
const navigation =
@ -54,7 +55,6 @@ const HomeScreen = () => {
const [walletDialog, setWalletDialog] = useState<boolean>(false);
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [network, setNetwork] = useState<string>('eth');
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
const [phrase, setPhrase] = useState('');

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, Image, ScrollView, View } from 'react-native';
import { ActivityIndicator, Button, Text, Appbar } from 'react-native-paper';
@ -60,48 +60,54 @@ const SignRequest = ({ route }: SignRequestProps) => {
);
}, [route.params]);
const retrieveData = async (
requestNetwork: string,
requestAddress: string,
requestMessage: string,
) => {
const requestAccount = await retrieveSingleAccount(
requestNetwork,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
const retrieveData = useCallback(
async (
requestNetwork: string,
requestAddress: string,
requestMessage: string,
) => {
const requestAccount = await retrieveSingleAccount(
requestNetwork,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
if (requestAccount !== account) {
setAccount(requestAccount);
}
if (requestMessage !== message) {
setMessage(decodeURIComponent(requestMessage));
}
if (requestNetwork !== network) {
setNetwork(requestNetwork);
}
setIsLoading(false);
};
if (requestAccount !== account) {
setAccount(requestAccount);
}
if (requestMessage !== message) {
setMessage(decodeURIComponent(requestMessage));
}
if (requestNetwork !== network) {
setNetwork(requestNetwork);
}
setIsLoading(false);
},
[account, message, navigation, network],
);
const sanitizePath = (path: string) => {
const regex = /^\/sign\/(eth|cosmos)\/(.+)\/(.+)$/;
const match = path.match(regex);
const sanitizePath = useCallback(
(path: string) => {
const regex = /^\/sign\/(eth|cosmos)\/(.+)\/(.+)$/;
const match = path.match(regex);
if (match) {
const [network, address, message] = match;
return {
network,
address,
message,
};
} else {
navigation.navigate('InvalidPath');
}
return null;
};
if (match) {
const [network, address, message] = match;
return {
network,
address,
message,
};
} else {
navigation.navigate('InvalidPath');
}
return null;
},
[navigation],
);
useEffect(() => {
if (route.path) {
@ -119,7 +125,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
route.params.address,
route.params.message,
);
}, [route]);
}, [retrieveData, sanitizePath, route]);
const handleWalletConnectRequest = async () => {
const { requestEvent } = route.params;

View File

@ -12,7 +12,7 @@ export default function WalletConnect() {
const { activeSessions, setActiveSessions } = useWalletConnect();
const disconnect = async (sessionId: string) => {
await web3wallet.disconnectSession({
await web3wallet!.disconnectSession({
topic: sessionId,
reason: getSdkError('USER_DISCONNECTED'),
});
@ -22,7 +22,7 @@ export default function WalletConnect() {
};
useEffect(() => {
const sessions = web3wallet.getActiveSessions();
const sessions = web3wallet!.getActiveSessions();
setActiveSessions(sessions);
}, [setActiveSessions]);

View File

@ -3,7 +3,7 @@
/**
* Types
*/
export type TCosmosChain = keyof typeof COSMOS_TESTNET_CHAINS;
export type TCosmosChain = keyof typeof COSMOS_CHAINS;
/**
* Chains
@ -27,11 +27,31 @@ export const COSMOS_TESTNET_CHAINS: Record<
},
};
export const COSMOS_MAINNET_CHAINS = {
'cosmos:cosmoshub-4': {
chainId: 'cosmoshub-4',
name: 'Cosmos Hub',
logo: '/chain-logos/cosmos-cosmoshub-4.png',
rgb: '107, 111, 147',
rpc: '',
namespace: 'cosmos',
},
};
export const COSMOS_CHAINS = {
...COSMOS_MAINNET_CHAINS,
...COSMOS_TESTNET_CHAINS,
};
/**
* Methods
*/
export const COSMOS_METHODS = {
export const COSMOS_SIGNING_METHODS = {
COSMOS_SIGN_DIRECT: 'cosmos_signDirect',
COSMOS_SIGN_AMINO: 'cosmos_signAmino',
};
export const COSMOS_METHODS = {
...COSMOS_SIGNING_METHODS,
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
};

View File

@ -64,6 +64,16 @@ export const EIP155_MAINNET_CHAINS: Record<string, EIP155Chain> = {
rpc: 'https://mainnet.era.zksync.io/',
namespace: 'eip155',
},
// Required chain by SIWE
'eip155:42161': {
chainId: 42161,
name: 'Arbitrum One',
logo: '',
rgb: '242, 242, 242',
rpc: 'https://arb1.arbitrum.io/rpc',
namespace: 'eip155',
},
};
export const EIP155_TEST_CHAINS: Record<string, EIP155Chain> = {

View File

@ -51,97 +51,3 @@ export default class EIP155Lib {
return this.wallet.signTransaction(transaction);
}
}
/**
* @desc Reference list of eip155 chains
* @url https://chainlist.org
*/
/**
* Types
*/
export type TEIP155Chain = keyof typeof EIP155_CHAINS;
/**
* Chains
*/
export const EIP155_MAINNET_CHAINS = {
'eip155:1': {
chainId: 1,
name: 'Ethereum',
logo: '/chain-logos/eip155-1.png',
rgb: '99, 125, 234',
rpc: 'https://cloudflare-eth.com/',
},
'eip155:43114': {
chainId: 43114,
name: 'Avalanche C-Chain',
logo: '/chain-logos/eip155-43113.png',
rgb: '232, 65, 66',
rpc: 'https://api.avax.network/ext/bc/C/rpc',
},
'eip155:137': {
chainId: 137,
name: 'Polygon',
logo: '/chain-logos/eip155-137.png',
rgb: '130, 71, 229',
rpc: 'https://polygon-rpc.com/',
},
'eip155:10': {
chainId: 10,
name: 'Optimism',
logo: '/chain-logos/eip155-10.png',
rgb: '235, 0, 25',
rpc: 'https://mainnet.optimism.io',
},
};
export const EIP155_TEST_CHAINS = {
'eip155:5': {
chainId: 5,
name: 'Ethereum Goerli',
logo: '/chain-logos/eip155-1.png',
rgb: '99, 125, 234',
rpc: 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
},
'eip155:43113': {
chainId: 43113,
name: 'Avalanche Fuji',
logo: '/chain-logos/eip155-43113.png',
rgb: '232, 65, 66',
rpc: 'https://api.avax-test.network/ext/bc/C/rpc',
},
'eip155:80001': {
chainId: 80001,
name: 'Polygon Mumbai',
logo: '/chain-logos/eip155-137.png',
rgb: '130, 71, 229',
rpc: 'https://matic-mumbai.chainstacklabs.com',
},
'eip155:420': {
chainId: 420,
name: 'Optimism Goerli',
logo: '/chain-logos/eip155-10.png',
rgb: '235, 0, 25',
rpc: 'https://goerli.optimism.io',
},
};
export const EIP155_CHAINS = {
...EIP155_MAINNET_CHAINS,
...EIP155_TEST_CHAINS,
};
/**
* Methods
*/
export const EIP155_SIGNING_METHODS = {
PERSONAL_SIGN: 'personal_sign',
ETH_SIGN: 'eth_sign',
ETH_SIGN_TRANSACTION: 'eth_signTransaction',
ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
ETH_SIGN_TYPED_DATA_V3: 'eth_signTypedData_v3',
ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4',
ETH_SEND_RAW_TRANSACTION: 'eth_sendRawTransaction',
ETH_SEND_TRANSACTION: 'eth_sendTransaction',
};

View File

@ -3,7 +3,7 @@
import { utils } from 'ethers';
import { Account } from '../../types';
import { EIP155_CHAINS, TEIP155Chain } from './EIP155Lib';
import { EIP155_CHAINS, TEIP155Chain } from './EIP155Data';
/**
* Truncates string (in the middle) via given lenght value

View File

@ -11,7 +11,7 @@ import {
calculateFee,
} from '@cosmjs/stargate';
import { EIP155_SIGNING_METHODS } from './EIP155Lib';
import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message';
import { Account } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
@ -60,9 +60,7 @@ export async function approveWalletConnectRequest(
}
const ethSignature = await signEthMessage(message, account.counterId);
return formatJsonRpcResult(id, {
signature: ethSignature,
});
return formatJsonRpcResult(id, ethSignature);
case 'cosmos_signDirect':
// Reference: https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmos/tx/v1beta1/tx.ts#L51

View File

@ -2814,6 +2814,11 @@
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/lodash@^4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==
"@types/node@*":
version "20.11.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708"