Take gas limit and fees values from user while approving transaction (#109)

* Take gas limit and fees from user

* Update text input ui

* Use gasPrice from networks data

* Use default gas limit from env

* Use default gas price if not found in registry

* Remove appended denom in gas price

* Use gas limit from env

* Show error dialog when transaction fails

* Calculate gas limit and gas price if not received from dapp

* Update example env

* Improve syntax

---------

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
This commit is contained in:
shreerang6921 2024-04-23 10:22:30 +05:30 committed by Nabarun Gogoi
parent ad202f46fc
commit b1a0831e78
12 changed files with 290 additions and 74 deletions

View File

@ -1 +1,3 @@
WALLET_CONNECT_PROJECT_ID=
DEFAULT_GAS_LIMIT=
DEFAULT_GAS_PRICE=

View File

@ -2,6 +2,8 @@
declare module 'react-native-config' {
export interface NativeConfig {
WALLET_CONNECT_PROJECT_ID: string;
DEFAULT_GAS_LIMIT: string;
DEFAULT_GAS_PRICE: string;
}
export const Config: NativeConfig;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Button, Dialog, Portal, Text } from 'react-native-paper';
const TxErrorDialog = ({
error,
visible,
hideDialog,
}: {
error: string;
visible: boolean;
hideDialog: () => void;
}) => {
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Title>Error sending transaction</Dialog.Title>
<Dialog.Content>
<Text variant="bodyMedium">{error}</Text>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={hideDialog}>OK</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default TxErrorDialog;

View File

@ -53,6 +53,10 @@ const cosmosNetworkDataSchema = z.object({
coinType: z.string().nonempty({ message: EMPTY_FIELD_ERROR }).regex(/^\d+$/),
nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
gasPrice: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(/^\d+(\.\d+)?$/),
});
const AddNetwork = () => {
@ -113,6 +117,13 @@ const AddNetwork = () => {
setValue('addressPrefix', cosmosChainDetails.bech32_prefix);
setValue('coinType', String(cosmosChainDetails.slip44 ?? '118'));
setValue('nativeDenom', cosmosChainDetails.fees?.fee_tokens[0].denom || '');
setValue(
'gasPrice',
String(
cosmosChainDetails.fees?.fee_tokens[0].average_gas_price ||
String(process.env.DEFAULT_GAS_PRICE),
),
);
}, CHAINID_DEBOUNCE_DELAY);
const submit = useCallback(
@ -359,6 +370,31 @@ const AddNetwork = () => {
</>
)}
/>
<Controller
control={control}
name="gasPrice"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Gas Price"
onBlur={onBlur}
onChangeText={onChange}
/>
<HelperText type="error">
{
(
errors as FieldErrors<
z.infer<typeof cosmosNetworkDataSchema>
>
).gasPrice?.message
}
</HelperText>
</>
)}
/>
</>
)}
<Button

View File

@ -1,7 +1,14 @@
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 { providers, BigNumber } from 'ethers';
import {
ActivityIndicator,
Button,
Text,
Appbar,
TextInput,
} from 'react-native-paper';
import { providers, BigNumber, ethers } from 'ethers';
import Config from 'react-native-config';
import { useNavigation } from '@react-navigation/native';
import {
@ -29,6 +36,7 @@ import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
import TxErrorDialog from '../components/TxErrorDialog';
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
@ -52,12 +60,20 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const [isTxLoading, setIsTxLoading] = useState(false);
const [cosmosStargateClient, setCosmosStargateClient] =
useState<SigningStargateClient>();
const [fees, setFees] = useState('');
const [cosmosGasLimit, setCosmosGasLimit] = useState(
Config.DEFAULT_GAS_LIMIT,
);
const [txError, setTxError] = useState<string>('');
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [ethGasPrice, setEthGasPrice] = useState<string>();
const [ethGasLimit, setEthGasLimit] = useState<string>();
const requestedChain = networksData.find(
const requestedNetwork = networksData.find(
networkData =>
`${networkData.namespace}:${networkData.chainId}` === chainId,
);
const namespace = requestedChain!.namespace;
const namespace = requestedNetwork!.namespace;
useEffect(() => {
if (namespace !== COSMOS) {
@ -71,18 +87,18 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const cosmosPrivKey = (
await getPathKey(
`${requestedChain?.namespace}:${requestedChain?.chainId}`,
`${requestedNetwork?.namespace}:${requestedNetwork?.chainId}`,
account.index,
)
).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedChain?.addressPrefix,
requestedNetwork?.addressPrefix,
);
const client = await SigningStargateClient.connectWithSigner(
requestedChain?.rpcUrl!,
requestedNetwork?.rpcUrl!,
sender,
);
@ -90,17 +106,17 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
};
setClient();
}, [account, requestedChain, chainId, namespace]);
}, [account, requestedNetwork, chainId, namespace]);
const provider = useMemo(() => {
if (namespace === EIP155) {
if (!requestedChain) {
if (!requestedNetwork) {
throw new Error('Requested chain not supported');
}
return new providers.JsonRpcProvider(requestedChain.rpcUrl);
return new providers.JsonRpcProvider(requestedNetwork.rpcUrl);
}
}, [requestedChain, namespace]);
}, [requestedNetwork, namespace]);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -108,8 +124,8 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const retrieveData = useCallback(
async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount(
requestedChain!.namespace,
requestedChain!.chainId,
requestedNetwork!.namespace,
requestedNetwork!.chainId,
requestAddress,
);
if (!requestAccount) {
@ -120,21 +136,31 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
setAccount(requestAccount);
setIsLoading(false);
},
[navigation, requestedChain],
[navigation, requestedNetwork],
);
const gasFees = useMemo(() => {
useEffect(() => {
if (namespace === EIP155) {
return BigNumber.from(transaction.gasLimit)
.mul(BigNumber.from(transaction.gasPrice))
const ethFees = BigNumber.from(transaction.gasLimit ?? ethGasLimit ?? 0)
.mul(BigNumber.from(transaction.gasPrice ?? ethGasPrice ?? 0))
.toString();
setFees(ethFees);
} else {
const gasPrice = GasPrice.fromString(transaction.gasPrice!.toString());
const cosmosFees = calculateFee(Number(transaction.gasLimit), gasPrice);
const gasPrice = GasPrice.fromString(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
return cosmosFees.amount[0].amount;
setFees(cosmosFees.amount[0].amount);
}
}, [transaction, namespace]);
}, [
transaction,
namespace,
ethGasLimit,
ethGasPrice,
cosmosGasLimit,
requestedNetwork,
]);
useEffect(() => {
retrieveData(transaction.from!);
@ -146,21 +172,41 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
throw new Error('account not found');
}
const response = await approveWalletConnectRequest(
networksData,
requestEvent,
account,
namespace!,
requestedChain!.chainId,
'',
namespace === EIP155 ? provider : cosmosStargateClient,
);
try {
const response = await approveWalletConnectRequest({
networksData,
requestEvent,
account,
namespace,
chainId: requestedNetwork!.chainId,
provider: namespace === EIP155 ? provider : cosmosStargateClient,
// StdFee object
cosmosFee: {
// This amount is total fees required for transaction
amount: [
{
amount: fees,
denom: requestedNetwork!.nativeDenom!,
},
],
gas: cosmosGasLimit,
},
ethGasLimit: transaction.gasLimit
? String(transaction.gasLimit)
: ethGasLimit,
ethGasPrice: transaction.gasPrice
? String(transaction.gasPrice)
: ethGasPrice,
});
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
navigation.navigate('Laconic');
} catch (error: any) {
setTxError(String(error));
setIsTxErrorDialogOpen(true);
}
setIsTxLoading(false);
navigation.navigate('Laconic');
};
const rejectRequestHandler = async () => {
@ -187,7 +233,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
} else {
const cosmosBalance = await cosmosStargateClient?.getBalance(
account.address,
requestedChain!.nativeDenom!.toLowerCase(),
requestedNetwork!.nativeDenom!.toLowerCase(),
);
setBalance(cosmosBalance?.amount!);
@ -195,7 +241,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
};
getAccountBalance();
}, [account, provider, namespace, cosmosStargateClient, requestedChain]);
}, [account, provider, namespace, cosmosStargateClient, requestedNetwork]);
useEffect(() => {
navigation.setOptions({
@ -221,6 +267,37 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, route.name]);
useEffect(() => {
const getEthGas = async () => {
if (transaction.gasLimit && transaction.gasPrice) {
return;
}
if (!provider) {
return;
}
const gasPriceVal = await provider.getGasPrice();
const gasPrice = ethers.utils.hexValue(gasPriceVal);
setEthGasPrice(String(gasPrice));
const transactionObject = {
from: transaction.from!,
to: transaction.to!,
data: transaction.data!,
gasPrice,
value: transaction.value!,
};
const gasLimit = await provider.estimateGas(transactionObject);
setEthGasLimit(String(gasLimit));
};
getEthGas();
}, [provider, transaction]);
return (
<>
{isLoading ? (
@ -248,7 +325,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</View>
<DataBox
label={`Balance (${
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={
balance === '' || balance === undefined
@ -261,20 +338,43 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
<DataBox label="To" data={transaction.to!} />
<DataBox
label={`Amount (${
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={BigNumber.from(
transaction.value?.toString(),
).toString()}
/>
<DataBox
label={`Gas Fees (${
namespace === EIP155 ? 'wei' : requestedChain!.nativeDenom
})`}
data={gasFees!}
/>
{namespace === EIP155 && (
<DataBox label="Data" data={transaction.data!} />
{namespace === EIP155 ? (
<>
<DataBox
label={`Gas Fees (${
namespace === EIP155
? 'wei'
: requestedNetwork!.nativeDenom
})`}
data={fees!}
/>
<DataBox label="Data" data={transaction.data!} />
</>
) : (
<>
<Text style={styles.dataBoxLabel}>{`Fees (${
requestedNetwork!.nativeDenom
})`}</Text>
<TextInput
mode="outlined"
value={fees}
onChangeText={value => setFees(value)}
style={styles.transactionFeesInput}
/>
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
<TextInput
mode="outlined"
value={cosmosGasLimit}
onChangeText={value => setCosmosGasLimit(value)}
/>
</>
)}
</View>
)}
@ -296,6 +396,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</View>
</>
)}
<TxErrorDialog
error={txError}
visible={isTxErrorDialogOpen}
hideDialog={() => setIsTxErrorDialogOpen(false)}
/>
</>
);
};

View File

@ -16,7 +16,11 @@ import { StackParamsList } from '../types';
import styles from '../styles/stylesheet';
import { retrieveNetworksData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext';
import { EMPTY_FIELD_ERROR, INVALID_URL_ERROR } from '../utils/constants';
import {
EIP155,
EMPTY_FIELD_ERROR,
INVALID_URL_ERROR,
} from '../utils/constants';
const networksFormDataSchema = z.object({
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
@ -25,6 +29,10 @@ const networksFormDataSchema = z.object({
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
gasPrice: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(/^\d+(\.\d+)?$/),
});
type EditNetworkProps = NativeStackScreenProps<StackParamsList, 'EditNetwork'>;
@ -129,6 +137,25 @@ const EditNetwork = ({ route }: EditNetworkProps) => {
</>
)}
/>
{networkData.namespace !== EIP155 ? (
<Controller
control={control}
name="gasPrice"
defaultValue={networkData.gasPrice}
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Gas Price"
onBlur={onBlur}
onChangeText={onChange}
/>
<HelperText type="error">{errors.gasPrice?.message}</HelperText>
</>
)}
/>
) : null}
<Button
mode="contained"
loading={isSubmitting}

View File

@ -159,14 +159,14 @@ const SignRequest = ({ route }: SignRequestProps) => {
throw new Error('Request event not found');
}
const response = await approveWalletConnectRequest(
const response = await approveWalletConnectRequest({
networksData,
requestEvent,
account,
namespace,
chainId,
message,
);
});
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });

View File

@ -271,6 +271,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginVertical: 10,
},
transactionFeesInput: { marginBottom: 10 },
});
export default styles;

View File

@ -57,6 +57,7 @@ export type NetworksFormData = {
nativeDenom?: string;
addressPrefix?: string;
coinType?: string;
gasPrice?: string;
isDefault: boolean;
};

View File

@ -135,7 +135,10 @@ const storeNetworkData = async (
const updatedNetworks: NetworksDataState[] = [
...retrievedNetworks,
{ ...networkData, networkId: String(networkId) },
{
...networkData,
networkId: String(networkId),
},
];
await setInternetCredentials(
'networks',

View File

@ -23,6 +23,7 @@ export const DEFAULT_NETWORKS = [
nativeDenom: 'uatom',
addressPrefix: 'cosmos',
coinType: '118',
gasPrice: '0.025',
isDefault: true,
},
];

View File

@ -4,12 +4,7 @@ import { Wallet, providers } from 'ethers';
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
import { SignClientTypes } from '@walletconnect/types';
import { getSdkError } from '@walletconnect/utils';
import {
SigningStargateClient,
coins,
GasPrice,
calculateFee,
} from '@cosmjs/stargate';
import { SigningStargateClient, coins, StdFee } from '@cosmjs/stargate';
import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message';
@ -17,15 +12,29 @@ import { Account, NetworksDataState } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
import { getCosmosAccounts } from '../accounts';
export async function approveWalletConnectRequest(
networksData: NetworksDataState[],
requestEvent: SignClientTypes.EventArguments['session_request'],
account: Account,
namespace: string,
chainId: string,
message?: string,
provider?: providers.JsonRpcProvider | SigningStargateClient,
) {
export async function approveWalletConnectRequest({
networksData,
requestEvent,
account,
namespace,
chainId,
message,
provider,
cosmosFee,
ethGasLimit,
ethGasPrice,
}: {
networksData: NetworksDataState[];
requestEvent: SignClientTypes.EventArguments['session_request'];
account: Account;
namespace: string;
chainId: string;
message?: string;
provider?: providers.JsonRpcProvider | SigningStargateClient;
cosmosFee?: StdFee;
ethGasLimit?: string;
ethGasPrice?: string;
}) {
const { params, id } = requestEvent;
const { request } = params;
@ -51,13 +60,18 @@ export async function approveWalletConnectRequest(
).privKey;
const wallet = new Wallet(privKey);
const sendTransaction = request.params[0];
const updatedTransaction = {
...sendTransaction,
gasLimit: ethGasLimit,
gasPrice: ethGasPrice,
};
if (!(provider instanceof providers.JsonRpcProvider)) {
throw new Error('Provider not found');
}
const connectedWallet = wallet.connect(provider);
const hash = await connectedWallet.sendTransaction(sendTransaction);
const hash = await connectedWallet.sendTransaction(updatedTransaction);
const receipt = typeof hash === 'string' ? hash : hash?.hash;
return formatJsonRpcResult(id, {
signature: receipt,
@ -118,20 +132,16 @@ export async function approveWalletConnectRequest(
request.params[0].value,
requestedChain!.nativeDenom!,
);
const gasPrice = GasPrice.fromString(
request.params[0].gasPrice.toString(),
);
const cosmosFee = calculateFee(
Number(request.params[0].gasLimit),
gasPrice,
);
const receiverAddress = request.params[0].to;
if (!(provider instanceof SigningStargateClient)) {
throw new Error('Cosmos stargate client not found');
}
if (!cosmosFee) {
throw new Error('Cosmos fee not found');
}
const result = await provider.sendTokens(
address,
receiverAddress,