forked from cerc-io/laconic-wallet
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:
parent
3cd4c51515
commit
0fa793bead
18
src/App.tsx
18
src/App.tsx
@ -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}
|
||||
|
17
src/components/DataBox.tsx
Normal file
17
src/components/DataBox.tsx
Normal 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;
|
228
src/screens/ApproveTransaction.tsx
Normal file
228
src/screens/ApproveTransaction.tsx
Normal 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;
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
|
10
src/types.ts
10
src/types.ts
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user