Make gas values editable on approve transaction (#113)

* Make eth gas limit and gas price editable on approve transaction

* Make max fee and max priority fee editable for EIP 1559 supported chains

* Refactor approve transaction code

* Use gas limit from dapp if sent

* Refactor regex to constants file
This commit is contained in:
shreerang6921 2024-04-25 12:42:01 +05:30 committed by Nabarun Gogoi
parent 820ac62615
commit e98b1e1f8e
5 changed files with 170 additions and 56 deletions

View File

@ -26,6 +26,7 @@ import {
CHAINID_DEBOUNCE_DELAY, CHAINID_DEBOUNCE_DELAY,
EMPTY_FIELD_ERROR, EMPTY_FIELD_ERROR,
INVALID_URL_ERROR, INVALID_URL_ERROR,
IS_NUMBER_REGEX,
} from '../utils/constants'; } from '../utils/constants';
import { getCosmosAccounts } from '../utils/accounts'; import { getCosmosAccounts } from '../utils/accounts';
import ETH_CHAINS from '../assets/ethereum-chains.json'; import ETH_CHAINS from '../assets/ethereum-chains.json';
@ -38,7 +39,10 @@ const ethNetworkDataSchema = z.object({
.string() .string()
.url({ message: INVALID_URL_ERROR }) .url({ message: INVALID_URL_ERROR })
.or(z.literal('')), .or(z.literal('')),
coinType: z.string().nonempty({ message: EMPTY_FIELD_ERROR }).regex(/^\d+$/), coinType: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(IS_NUMBER_REGEX),
currencySymbol: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), currencySymbol: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
}); });
@ -50,7 +54,10 @@ const cosmosNetworkDataSchema = z.object({
.string() .string()
.url({ message: INVALID_URL_ERROR }) .url({ message: INVALID_URL_ERROR })
.or(z.literal('')), .or(z.literal('')),
coinType: z.string().nonempty({ message: EMPTY_FIELD_ERROR }).regex(/^\d+$/), coinType: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(IS_NUMBER_REGEX),
nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
gasPrice: z gasPrice: z

View File

@ -7,8 +7,9 @@ import {
Appbar, Appbar,
TextInput, TextInput,
} from 'react-native-paper'; } from 'react-native-paper';
import { providers, BigNumber, ethers } from 'ethers'; import { providers, BigNumber } from 'ethers';
import Config from 'react-native-config'; import Config from 'react-native-config';
import { Deferrable } from 'ethers/lib/utils';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { import {
@ -36,10 +37,12 @@ import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import DataBox from '../components/DataBox'; import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc'; import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants'; import { COSMOS, EIP155, IS_NUMBER_REGEX } from '../utils/constants';
import TxErrorDialog from '../components/TxErrorDialog'; import TxErrorDialog from '../components/TxErrorDialog';
const MEMO = 'Sending signed tx from Laconic Wallet'; const MEMO = 'Sending signed tx from Laconic Wallet';
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
const ETH_MINIMUM_GAS = 21000;
type SignRequestProps = NativeStackScreenProps< type SignRequestProps = NativeStackScreenProps<
StackParamsList, StackParamsList,
@ -67,8 +70,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>(); const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
const [txError, setTxError] = useState<string>(); const [txError, setTxError] = useState<string>();
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false); const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [ethGasPrice, setEthGasPrice] = useState<string>(); const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>();
const [ethGasLimit, setEthGasLimit] = useState<string>(); const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
useState<BigNumber | null>();
const isSufficientFunds = useMemo(() => { const isSufficientFunds = useMemo(() => {
if (!transaction.value) { if (!transaction.value) {
@ -183,15 +189,27 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
} }
setAccount(requestAccount); setAccount(requestAccount);
setIsLoading(false);
}, },
[navigation, requestedNetwork], [navigation, requestedNetwork],
); );
useEffect(() => {
// Set loading to false when gas values for requested chain are fetched
// If requested chain is EVM compatible, the cosmos gas values will be undefined and vice-versa, hence the condition checks only one of them at the same time
if (
// If requested chain is EVM compatible, set loading to false when ethMaxFee and ethPriorityFee have been populated
(ethMaxFee !== undefined && ethMaxPriorityFee !== undefined) ||
// Or if requested chain is a cosmos chain, set loading to false when cosmosGasLimit has been populated
cosmosGasLimit !== undefined
) {
setIsLoading(false);
}
}, [ethMaxFee, ethMaxPriorityFee, cosmosGasLimit]);
useEffect(() => { useEffect(() => {
if (namespace === EIP155) { if (namespace === EIP155) {
const ethFees = BigNumber.from(transaction.gasLimit ?? ethGasLimit ?? 0) const ethFees = BigNumber.from(ethGasLimit ?? 0)
.mul(BigNumber.from(transaction.gasPrice ?? ethGasPrice ?? 0)) .mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
.toString(); .toString();
setFees(ethFees); setFees(ethFees);
} else { } else {
@ -214,18 +232,39 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
ethGasPrice, ethGasPrice,
cosmosGasLimit, cosmosGasLimit,
requestedNetwork, requestedNetwork,
ethMaxFee,
]); ]);
useEffect(() => { useEffect(() => {
retrieveData(transaction.from!); retrieveData(transaction.from!);
}, [retrieveData, transaction]); }, [retrieveData, transaction]);
const isEIP1559 = useMemo(() => {
if (cosmosGasLimit) {
return;
}
if (ethMaxFee !== null && ethMaxPriorityFee !== null) {
return true;
}
return false;
}, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]);
const acceptRequestHandler = async () => { const acceptRequestHandler = async () => {
setIsTxLoading(true); setIsTxLoading(true);
if (!account) {
throw new Error('account not found');
}
try { try {
if (!account) {
throw new Error('account not found');
}
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
}
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
throw new Error(
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
);
}
const response = await approveWalletConnectRequest({ const response = await approveWalletConnectRequest({
requestEvent, requestEvent,
account, account,
@ -244,12 +283,10 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
gas: cosmosGasLimit!, gas: cosmosGasLimit!,
}, },
ethGasLimit: ethGasLimit:
namespace === EIP155 namespace === EIP155 ? BigNumber.from(ethGasLimit) : undefined,
? BigNumber.from(transaction.gasLimit ?? ethGasLimit) ethGasPrice: ethGasPrice?.toHexString(),
: undefined, maxPriorityFeePerGas: ethMaxPriorityFee ?? undefined,
ethGasPrice: transaction.gasPrice maxFeePerGas: ethMaxFee ?? undefined,
? String(transaction.gasPrice)
: ethGasPrice,
sendMsg, sendMsg,
memo: MEMO, memo: MEMO,
}); });
@ -331,34 +368,33 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
useEffect(() => { useEffect(() => {
const getEthGas = async () => { const getEthGas = async () => {
try { try {
if (transaction.gasLimit && transaction.gasPrice) { if (!isSufficientFunds || !provider) {
return; return;
} }
if (!isSufficientFunds) { const data = await provider.getFeeData();
return;
setEthMaxFee(data.maxFeePerGas);
setEthMaxPriorityFee(data.maxPriorityFeePerGas);
setEthGasPrice(data.gasPrice);
if (transaction.gasLimit) {
setEthGasLimit(BigNumber.from(transaction.gasLimit));
} else {
const transactionObject: Deferrable<providers.TransactionRequest> = {
from: transaction.from!,
to: transaction.to!,
data: transaction.data!,
value: transaction.value!,
maxFeePerGas: data.maxFeePerGas ?? undefined,
maxPriorityFeePerGas: data.maxPriorityFeePerGas ?? undefined,
gasPrice: data.maxFeePerGas
? undefined
: data.gasPrice ?? undefined,
};
const gasLimit = await provider.estimateGas(transactionObject);
setEthGasLimit(gasLimit);
} }
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));
} catch (error: any) { } catch (error: any) {
setTxError(error.message); setTxError(error.message);
setIsTxErrorDialogOpen(true); setIsTxErrorDialogOpen(true);
@ -452,19 +488,72 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
{namespace === EIP155 ? ( {namespace === EIP155 ? (
<> <>
{isEIP1559 === false ? (
<>
<Text style={styles.dataBoxLabel}>
{'Gas Price (wei)'}
</Text>
<TextInput
mode="outlined"
value={ethGasPrice?.toNumber().toString()}
onChangeText={value =>
setEthGasPrice(BigNumber.from(value))
}
style={styles.transactionFeesInput}
/>
</>
) : (
<>
<Text style={styles.dataBoxLabel}>
Max Fee Per Gas (wei)
</Text>
<TextInput
mode="outlined"
value={ethMaxFee?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthMaxFee(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
<Text style={styles.dataBoxLabel}>
Max Priority Fee Per Gas (wei)
</Text>
<TextInput
mode="outlined"
value={ethMaxPriorityFee?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthMaxPriorityFee(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
</>
)}
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
<TextInput
mode="outlined"
value={ethGasLimit?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthGasLimit(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
<DataBox <DataBox
label={`Gas Fees (${ label={`${
namespace === EIP155 isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
? 'wei' } (wei)`}
: requestedNetwork!.nativeDenom
})`}
data={fees!} data={fees!}
/> />
<DataBox label="Data" data={transaction.data!} /> <DataBox label="Data" data={transaction.data!} />
</> </>
) : ( ) : (
<> <>
<Text style={styles.dataBoxLabel}>{`Fees (${ <Text style={styles.dataBoxLabel}>{`Fee (${
requestedNetwork!.nativeDenom requestedNetwork!.nativeDenom
})`}</Text> })`}</Text>
<TextInput <TextInput
@ -477,7 +566,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
<TextInput <TextInput
mode="outlined" mode="outlined"
value={cosmosGasLimit} value={cosmosGasLimit}
onChangeText={value => setCosmosGasLimit(value)} onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setCosmosGasLimit(value);
}
}}
/> />
</> </>
)} )}

View File

@ -160,7 +160,6 @@ const SignRequest = ({ route }: SignRequestProps) => {
} }
const response = await approveWalletConnectRequest({ const response = await approveWalletConnectRequest({
networksData,
requestEvent, requestEvent,
account, account,
namespace, namespace,

View File

@ -32,3 +32,5 @@ export const CHAINID_DEBOUNCE_DELAY = 250;
export const EMPTY_FIELD_ERROR = 'Field cannot be empty'; export const EMPTY_FIELD_ERROR = 'Field cannot be empty';
export const INVALID_URL_ERROR = 'Invalid URL'; export const INVALID_URL_ERROR = 'Invalid URL';
export const IS_NUMBER_REGEX = /^\d+$/;

View File

@ -28,6 +28,8 @@ export async function approveWalletConnectRequest({
ethGasPrice, ethGasPrice,
sendMsg, sendMsg,
memo, memo,
maxPriorityFeePerGas,
maxFeePerGas,
}: { }: {
requestEvent: SignClientTypes.EventArguments['session_request']; requestEvent: SignClientTypes.EventArguments['session_request'];
account: Account; account: Account;
@ -38,6 +40,8 @@ export async function approveWalletConnectRequest({
cosmosFee?: StdFee; cosmosFee?: StdFee;
ethGasLimit?: BigNumber; ethGasLimit?: BigNumber;
ethGasPrice?: string; ethGasPrice?: string;
maxPriorityFeePerGas?: BigNumber;
maxFeePerGas?: BigNumber;
sendMsg?: MsgSendEncodeObject; sendMsg?: MsgSendEncodeObject;
memo?: string; memo?: string;
}) { }) {
@ -61,11 +65,20 @@ export async function approveWalletConnectRequest({
).privKey; ).privKey;
const wallet = new Wallet(privKey); const wallet = new Wallet(privKey);
const sendTransaction = request.params[0]; const sendTransaction = request.params[0];
const updatedTransaction = { const updatedTransaction =
...sendTransaction, maxFeePerGas && maxPriorityFeePerGas
gasLimit: ethGasLimit, ? {
gasPrice: ethGasPrice, ...sendTransaction,
}; gasLimit: ethGasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
}
: {
...sendTransaction,
gasLimit: ethGasLimit,
gasPrice: ethGasPrice,
type: 0,
};
if (!(provider instanceof providers.JsonRpcProvider)) { if (!(provider instanceof providers.JsonRpcProvider)) {
throw new Error('Provider not found'); throw new Error('Provider not found');