diff --git a/src/screens/AddNetwork.tsx b/src/screens/AddNetwork.tsx index a2b5947..5948500 100644 --- a/src/screens/AddNetwork.tsx +++ b/src/screens/AddNetwork.tsx @@ -26,6 +26,7 @@ import { CHAINID_DEBOUNCE_DELAY, EMPTY_FIELD_ERROR, INVALID_URL_ERROR, + IS_NUMBER_REGEX, } from '../utils/constants'; import { getCosmosAccounts } from '../utils/accounts'; import ETH_CHAINS from '../assets/ethereum-chains.json'; @@ -38,7 +39,10 @@ const ethNetworkDataSchema = z.object({ .string() .url({ message: INVALID_URL_ERROR }) .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 }), }); @@ -50,7 +54,10 @@ const cosmosNetworkDataSchema = z.object({ .string() .url({ message: INVALID_URL_ERROR }) .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 }), addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), gasPrice: z diff --git a/src/screens/ApproveTransaction.tsx b/src/screens/ApproveTransaction.tsx index 6e839d6..c424993 100644 --- a/src/screens/ApproveTransaction.tsx +++ b/src/screens/ApproveTransaction.tsx @@ -7,8 +7,9 @@ import { Appbar, TextInput, } from 'react-native-paper'; -import { providers, BigNumber, ethers } from 'ethers'; +import { providers, BigNumber } from 'ethers'; import Config from 'react-native-config'; +import { Deferrable } from 'ethers/lib/utils'; import { useNavigation } from '@react-navigation/native'; import { @@ -36,10 +37,12 @@ import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils'; import DataBox from '../components/DataBox'; import { getPathKey } from '../utils/misc'; 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'; 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< StackParamsList, @@ -67,8 +70,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { const [cosmosGasLimit, setCosmosGasLimit] = useState(); const [txError, setTxError] = useState(); const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false); - const [ethGasPrice, setEthGasPrice] = useState(); - const [ethGasLimit, setEthGasLimit] = useState(); + const [ethGasPrice, setEthGasPrice] = useState(); + const [ethGasLimit, setEthGasLimit] = useState(); + const [ethMaxFee, setEthMaxFee] = useState(); + const [ethMaxPriorityFee, setEthMaxPriorityFee] = + useState(); const isSufficientFunds = useMemo(() => { if (!transaction.value) { @@ -183,15 +189,27 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { } setAccount(requestAccount); - setIsLoading(false); }, [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(() => { if (namespace === EIP155) { - const ethFees = BigNumber.from(transaction.gasLimit ?? ethGasLimit ?? 0) - .mul(BigNumber.from(transaction.gasPrice ?? ethGasPrice ?? 0)) + const ethFees = BigNumber.from(ethGasLimit ?? 0) + .mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0)) .toString(); setFees(ethFees); } else { @@ -214,18 +232,39 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { ethGasPrice, cosmosGasLimit, requestedNetwork, + ethMaxFee, ]); useEffect(() => { retrieveData(transaction.from!); }, [retrieveData, transaction]); + const isEIP1559 = useMemo(() => { + if (cosmosGasLimit) { + return; + } + if (ethMaxFee !== null && ethMaxPriorityFee !== null) { + return true; + } + return false; + }, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]); + const acceptRequestHandler = async () => { setIsTxLoading(true); - if (!account) { - throw new Error('account not found'); - } - 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({ requestEvent, account, @@ -244,12 +283,10 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { gas: cosmosGasLimit!, }, ethGasLimit: - namespace === EIP155 - ? BigNumber.from(transaction.gasLimit ?? ethGasLimit) - : undefined, - ethGasPrice: transaction.gasPrice - ? String(transaction.gasPrice) - : ethGasPrice, + namespace === EIP155 ? BigNumber.from(ethGasLimit) : undefined, + ethGasPrice: ethGasPrice?.toHexString(), + maxPriorityFeePerGas: ethMaxPriorityFee ?? undefined, + maxFeePerGas: ethMaxFee ?? undefined, sendMsg, memo: MEMO, }); @@ -331,34 +368,33 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { useEffect(() => { const getEthGas = async () => { try { - if (transaction.gasLimit && transaction.gasPrice) { + if (!isSufficientFunds || !provider) { return; } - if (!isSufficientFunds) { - return; + const data = await provider.getFeeData(); + + setEthMaxFee(data.maxFeePerGas); + setEthMaxPriorityFee(data.maxPriorityFeePerGas); + setEthGasPrice(data.gasPrice); + + if (transaction.gasLimit) { + setEthGasLimit(BigNumber.from(transaction.gasLimit)); + } else { + const transactionObject: Deferrable = { + 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) { setTxError(error.message); setIsTxErrorDialogOpen(true); @@ -452,19 +488,72 @@ const ApproveTransaction = ({ route }: SignRequestProps) => { {namespace === EIP155 ? ( <> + {isEIP1559 === false ? ( + <> + + {'Gas Price (wei)'} + + + setEthGasPrice(BigNumber.from(value)) + } + style={styles.transactionFeesInput} + /> + + ) : ( + <> + + Max Fee Per Gas (wei) + + { + if (IS_NUMBER_REGEX.test(value)) { + setEthMaxFee(BigNumber.from(value)); + } + }} + style={styles.transactionFeesInput} + /> + + Max Priority Fee Per Gas (wei) + + { + if (IS_NUMBER_REGEX.test(value)) { + setEthMaxPriorityFee(BigNumber.from(value)); + } + }} + style={styles.transactionFeesInput} + /> + + )} + Gas Limit + { + if (IS_NUMBER_REGEX.test(value)) { + setEthGasLimit(BigNumber.from(value)); + } + }} + style={styles.transactionFeesInput} + /> ) : ( <> - {`Fees (${ + {`Fee (${ requestedNetwork!.nativeDenom })`} { setCosmosGasLimit(value)} + onChangeText={value => { + if (IS_NUMBER_REGEX.test(value)) { + setCosmosGasLimit(value); + } + }} /> )} diff --git a/src/screens/SignRequest.tsx b/src/screens/SignRequest.tsx index c0d251a..64dc03b 100644 --- a/src/screens/SignRequest.tsx +++ b/src/screens/SignRequest.tsx @@ -160,7 +160,6 @@ const SignRequest = ({ route }: SignRequestProps) => { } const response = await approveWalletConnectRequest({ - networksData, requestEvent, account, namespace, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6331210..c4956fc 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -32,3 +32,5 @@ export const CHAINID_DEBOUNCE_DELAY = 250; export const EMPTY_FIELD_ERROR = 'Field cannot be empty'; export const INVALID_URL_ERROR = 'Invalid URL'; + +export const IS_NUMBER_REGEX = /^\d+$/; diff --git a/src/utils/wallet-connect/WalletConnectRequests.ts b/src/utils/wallet-connect/WalletConnectRequests.ts index 3a71608..db205ad 100644 --- a/src/utils/wallet-connect/WalletConnectRequests.ts +++ b/src/utils/wallet-connect/WalletConnectRequests.ts @@ -28,6 +28,8 @@ export async function approveWalletConnectRequest({ ethGasPrice, sendMsg, memo, + maxPriorityFeePerGas, + maxFeePerGas, }: { requestEvent: SignClientTypes.EventArguments['session_request']; account: Account; @@ -38,6 +40,8 @@ export async function approveWalletConnectRequest({ cosmosFee?: StdFee; ethGasLimit?: BigNumber; ethGasPrice?: string; + maxPriorityFeePerGas?: BigNumber; + maxFeePerGas?: BigNumber; sendMsg?: MsgSendEncodeObject; memo?: string; }) { @@ -61,11 +65,20 @@ export async function approveWalletConnectRequest({ ).privKey; const wallet = new Wallet(privKey); const sendTransaction = request.params[0]; - const updatedTransaction = { - ...sendTransaction, - gasLimit: ethGasLimit, - gasPrice: ethGasPrice, - }; + const updatedTransaction = + maxFeePerGas && maxPriorityFeePerGas + ? { + ...sendTransaction, + gasLimit: ethGasLimit, + maxFeePerGas, + maxPriorityFeePerGas, + } + : { + ...sendTransaction, + gasLimit: ethGasLimit, + gasPrice: ethGasPrice, + type: 0, + }; if (!(provider instanceof providers.JsonRpcProvider)) { throw new Error('Provider not found');