Add new page for approving eth transactions (#63)

* Display funds on signRequest page

* Format balance value

* Display upto 18 digits

* Use useMemo for provider

* Display balance in wei

* Make UI changes

* Make review changes

* Add page to approve eth transactions

* Update approve transaction page ui

* Update balance unit display

---------

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
This commit is contained in:
IshaVenikar 2024-03-21 16:09:25 +05:30 committed by GitHub
parent 3cd4c51515
commit 0fa793bead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 323 additions and 26 deletions

View File

@ -25,6 +25,7 @@ import { StackParamsList } from './types';
import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Lib';
import { getSignParamsMessage } from './utils/wallet-connect/Helpers';
import ApproveTransaction from './screens/ApproveTransaction';
const Stack = createNativeStackNavigator<StackParamsList>();
@ -65,10 +66,9 @@ const App = (): React.JSX.Element => {
web3wallet!.engine.signClient.session.get(topic);
switch (request.method) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
navigation.navigate('SignRequest', {
navigation.navigate('ApproveTransaction', {
network: 'eth',
address: request.params[0].from,
message: JSON.stringify(request.params[0], undefined, 2),
transaction: request.params[0],
requestEvent,
requestSessionData,
});
@ -167,9 +167,7 @@ const App = (): React.JSX.Element => {
name="SignRequest"
component={SignRequest}
options={{
headerTitle: () => (
<Text variant="titleLarge">Sign this message?</Text>
),
headerTitle: () => <Text variant="titleLarge">Sign Request</Text>,
}}
/>
<Stack.Screen
@ -203,6 +201,14 @@ const App = (): React.JSX.Element => {
title: 'New session',
}}
/>
<Stack.Screen
name="ApproveTransaction"
component={ApproveTransaction}
options={{
title: 'Approve transaction',
}}
/>
</Stack.Navigator>
<PairingModal
visible={modalVisible}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { View, Text } from 'react-native';
import styles from '../styles/stylesheet';
const DataBox = ({ label, data }: { label: string; data: string }) => {
return (
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>{label}</Text>
<View style={styles.dataBox}>
<Text style={styles.dataBoxData}>{data}</Text>
</View>
</View>
);
};
export default DataBox;

View File

@ -0,0 +1,228 @@
import React, { 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 { useNavigation } from '@react-navigation/native';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { getHeaderTitle } from '@react-navigation/elements';
import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import { retrieveSingleAccount } from '../utils/accounts';
import {
approveWalletConnectRequest,
rejectWalletConnectRequest,
} from '../utils/wallet-connect/WalletConnectRequests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import {
TEIP155Chain,
EIP155_CHAINS,
} from '../utils/wallet-connect/EIP155Data';
import DataBox from '../components/DataBox';
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
'ApproveTransaction'
>;
const ApproveTransaction = ({ route }: SignRequestProps) => {
const requestSession = route.params?.requestSessionData;
const requestName = requestSession?.peer?.metadata?.name;
const requestIcon = requestSession?.peer?.metadata?.icons[0];
const requestURL = requestSession?.peer?.metadata?.url;
const [account, setAccount] = useState<Account>();
const [transactionData, setTransactionData] =
useState<PopulatedTransaction>();
const [network, setNetwork] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [balance, setBalance] = useState<string>('');
const requestParams = route.params!.requestEvent;
const chainId = requestParams?.params.chainId;
const provider = useMemo(() => {
return new providers.JsonRpcProvider(
EIP155_CHAINS[chainId as TEIP155Chain].rpc,
);
}, [chainId]);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const retrieveData = async (
requestNetwork: string,
requestAddress: string,
) => {
const requestAccount = await retrieveSingleAccount(
requestNetwork,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
if (requestAccount !== account) {
setAccount(requestAccount);
}
if (requestNetwork !== network) {
setNetwork(requestNetwork);
}
if (route.params?.transaction) {
setTransactionData(route.params?.transaction);
}
setIsLoading(false);
};
const gasFees = useMemo(() => {
if (!transactionData) {
return;
}
return BigNumber.from(transactionData?.gasLimit)
.mul(BigNumber.from(transactionData?.gasPrice))
.toString();
}, [transactionData]);
useEffect(() => {
route.params &&
retrieveData(route.params?.network, route.params?.transaction.from!);
}, [route]);
const acceptRequestHandler = async () => {
const { requestEvent } = route.params || {};
if (!account) {
throw new Error('account not found');
}
if (!requestEvent) {
throw new Error('Request event not found');
}
const response = await approveWalletConnectRequest(
requestEvent,
account,
network,
'',
provider,
);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
navigation.navigate('Laconic');
};
const rejectRequestHandler = async () => {
if (route.params?.requestEvent) {
const response = rejectWalletConnectRequest(route.params?.requestEvent);
const { topic } = route.params?.requestEvent;
await web3wallet!.respondSessionRequest({
topic,
response,
});
}
navigation.navigate('Laconic');
};
useEffect(() => {
const getAccountBalance = async (account: Account) => {
const fetchedBalance = await provider.getBalance(account.address);
setBalance(fetchedBalance.toString());
};
if (account) {
getAccountBalance(account);
}
}, [account, provider]);
useEffect(() => {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
header: ({ options, back }) => {
const title = getHeaderTitle(options, 'Approve Transaction');
return (
<Appbar.Header>
{back && (
<Appbar.BackAction
onPress={async () => {
await rejectRequestHandler();
navigation.navigate('Laconic');
}}
/>
)}
<Appbar.Content title={title} />
</Appbar.Header>
);
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, route.name]);
return (
<>
{isLoading ? (
<View style={styles.spinnerContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : (
<ScrollView style={styles.appContainer}>
<View style={styles.dappDetails}>
{requestIcon && (
<Image
style={styles.dappLogo}
source={requestIcon ? { uri: requestIcon } : undefined}
/>
)}
<Text>{requestName}</Text>
<Text variant="bodyMedium">{requestURL}</Text>
</View>
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>From</Text>
<View style={styles.dataBox}>
<AccountDetails account={account} />
</View>
</View>
<DataBox
label="Balance (Wei)"
data={balance !== '' ? `${balance}` : 'Loading balance...'}
/>
{transactionData && (
<View style={styles.approveTransaction}>
<DataBox label="To" data={transactionData.to!} />
<DataBox
label="Amount (Wei)"
data={BigNumber.from(
transactionData.value?.toString(),
).toString()}
/>
<DataBox label="Gas Fees (Wei)" data={gasFees!} />
<DataBox label="Data" data={transactionData.data!} />
</View>
)}
<View style={styles.buttonContainer}>
<Button mode="contained" onPress={acceptRequestHandler}>
Yes
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D">
No
</Button>
</View>
</ScrollView>
)}
</>
);
};
export default ApproveTransaction;

View File

@ -185,7 +185,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
header: ({ options, back }) => {
const title = getHeaderTitle(options, route.name);
const title = getHeaderTitle(options, 'Sign Request');
return (
<Appbar.Header>
@ -212,7 +212,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : (
<View style={styles.appContainer}>
<ScrollView style={styles.appContainer}>
<View style={styles.dappDetails}>
{requestIcon && (
<Image
@ -224,7 +224,6 @@ const SignRequest = ({ route }: SignRequestProps) => {
<Text variant="bodyMedium">{requestURL}</Text>
</View>
<AccountDetails account={account} />
{isCosmosSignDirect || isEthSendTransaction ? (
<View style={styles.requestDirectMessage}>
<ScrollView nestedScrollEnabled>
@ -248,7 +247,7 @@ const SignRequest = ({ route }: SignRequestProps) => {
No
</Button>
</View>
</View>
</ScrollView>
)}
</>
);

View File

@ -18,7 +18,8 @@ const styles = StyleSheet.create({
fontWeight: '700',
},
accountContainer: {
marginTop: 12,
padding: 8,
paddingBottom: 0,
},
addAccountButton: {
marginTop: 24,
@ -129,6 +130,9 @@ const styles = StyleSheet.create({
justifyContent: 'center',
padding: 8,
},
approveTransaction: {
height: '40%',
},
buttonContainer: {
marginTop: 50,
flexDirection: 'row',
@ -216,6 +220,34 @@ const styles = StyleSheet.create({
display: 'flex',
alignItems: 'center',
},
dataBoxContainer: {
marginBottom: 10,
},
dataBoxLabel: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 3,
color: 'black',
},
dataBox: {
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
borderRadius: 5,
},
dataBoxData: {
fontSize: 16,
color: 'black',
},
transactionText: {
padding: 8,
fontSize: 18,
fontWeight: 'bold',
color: 'black',
},
balancePadding: {
padding: 8,
},
});
export default styles;

View File

@ -1,3 +1,5 @@
import { PopulatedTransaction } from 'ethers';
import { SignClientTypes, SessionTypes } from '@walletconnect/types';
import { Web3WalletTypes } from '@walletconnect/web3wallet';
@ -13,6 +15,14 @@ export type StackParamsList = {
requestSessionData?: SessionTypes.Struct;
}
| undefined;
ApproveTransaction:
| {
network: string;
transaction: PopulatedTransaction;
requestEvent?: Web3WalletTypes.SessionRequest;
requestSessionData?: SessionTypes.Struct;
}
| undefined;
InvalidPath: undefined;
WalletConnect: undefined;
AddSession: undefined;

View File

@ -10,16 +10,16 @@ import { signDirectMessage, signEthMessage } from '../sign-message';
import { Account } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
import { getCosmosAccounts } from '../accounts';
import { TEIP155Chain, EIP155_CHAINS } from './EIP155Data';
export async function approveWalletConnectRequest(
requestEvent: SignClientTypes.EventArguments['session_request'],
account: Account,
network: string,
message: string,
message?: string,
provider?: providers.JsonRpcProvider,
) {
const { params, id } = requestEvent;
const { request, chainId } = params;
const { request } = params;
const path = (await getPathKey(network, account.counterId)).path;
const mnemonic = await getMnemonic();
@ -27,7 +27,24 @@ export async function approveWalletConnectRequest(
const address = cosmosAccount.data.address;
switch (request.method) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
if (!provider) {
throw new Error('JSON RPC provider not found');
}
const privKey = (await getPathKey('eth', account.counterId)).privKey;
const wallet = new Wallet(privKey);
const sendTransaction = request.params[0];
const connectedWallet = await wallet.connect(provider);
const hash = await connectedWallet.sendTransaction(sendTransaction);
const receipt = typeof hash === 'string' ? hash : hash?.hash;
return formatJsonRpcResult(id, receipt);
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
if (!message) {
throw new Error('Message to be signed not found');
}
const ethSignature = await signEthMessage(message, account.counterId);
return formatJsonRpcResult(id, ethSignature);
@ -69,18 +86,6 @@ export async function approveWalletConnectRequest(
signature: cosmosAminoSignature.signature.signature,
});
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
const provider = new providers.JsonRpcProvider(
EIP155_CHAINS[chainId as TEIP155Chain].rpc,
);
const privKey = (await getPathKey('eth', account.counterId)).privKey;
const wallet = new Wallet(privKey);
const sendTransaction = request.params[0];
const connectedWallet = await wallet.connect(provider);
const hash = await connectedWallet.sendTransaction(sendTransaction);
const receipt = typeof hash === 'string' ? hash : hash?.hash;
return formatJsonRpcResult(id, receipt);
default:
throw new Error(getSdkError('INVALID_METHOD').message);
}