Skip to content

Commit

Permalink
Estimate gas price via provider
Browse files Browse the repository at this point in the history
  • Loading branch information
lubej committed Dec 14, 2023
1 parent 1516d58 commit 5e8cacc
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 36 deletions.
29 changes: 21 additions & 8 deletions src/components/WrapForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, FormEvent, MouseEvent, useEffect, useState } from 'react'
import { FC, FormEvent, MouseEvent, useEffect, useRef, useState } from 'react'
import { Input } from '../Input'
import classes from './index.module.css'
import { Button } from '../Button'
Expand All @@ -8,6 +8,8 @@ import { useNavigate } from 'react-router-dom'
import { ToggleButton } from '../ToggleButton'
import { useWrapForm } from '../../hooks/useWrapForm'
import { WrapFormType } from '../../utils/types'
import { useInterval } from '../../hooks/useInterval'
import { NumberUtils } from '../../utils/number.utils'

const AMOUNT_PATTERN = '^[0-9]*[.,]?[0-9]*$'

Expand All @@ -33,14 +35,25 @@ const labelMapByFormType: Record<WrapFormType, WrapFormLabels> = {
export const WrapForm: FC = () => {
const navigate = useNavigate()
const {
state: { formType, amount, isLoading, balance },
state: { formType, amount, isLoading, balance, estimatedFee },
toggleFormType,
submit,
getFeeAmount,
debounceLeadingSetFeeAmount,
} = useWrapForm()
const { firstInputLabel, secondInputLabel, submitBtnLabel } = labelMapByFormType[formType]
const [value, setValue] = useState('')
const [error, setError] = useState('')
const debouncedSetFeeAmount = useRef(debounceLeadingSetFeeAmount())

useEffect(() => {
// Trigger fee calculation on value change
debouncedSetFeeAmount.current()
}, [value])

useInterval(() => {
// Trigger fee calculation every minute, in case fee data becomes stale
debouncedSetFeeAmount.current()
}, 60000)

useEffect(() => {
setError('')
Expand Down Expand Up @@ -76,9 +89,10 @@ export const WrapForm: FC = () => {

const parsedValue = formType === WrapFormType.WRAP && value ? utils.parseUnits(value || '0', 'ether') : null
const showRoseMaxAmountWarning =
parsedValue && parsedValue.gt(0)
? utils.parseUnits(value, 'ether').eq(balance.sub(getFeeAmount()))
: false
parsedValue && parsedValue.gt(0) ? parsedValue.eq(balance.sub(estimatedFee)) : false

const estimatedFeeTruncated =
estimatedFee && estimatedFee.gt(0) ? `~${NumberUtils.getTruncatedAmount(estimatedFee)} ROSE` : '/'

return (
<div>
Expand Down Expand Up @@ -107,8 +121,7 @@ export const WrapForm: FC = () => {
<ToggleButton className={classes.toggleBtn} onClick={handleToggleFormType} disabled={isLoading} />
</div>

{/*This is hardcoded for now, as are gas prices*/}
<h4 className={classes.gasEstimateLabel}>Estimated fee: &lt;0.01 ROSE (~10 sec)</h4>
<h4 className={classes.gasEstimateLabel}>Estimated fee: {estimatedFeeTruncated}</h4>

<Button disabled={isLoading} type="submit" fullWidth>
{submitBtnLabel}
Expand Down
2 changes: 2 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export const NETWORKS: Record<number, NetworkConfiguration> = {
},
}

export const MAX_GAS_LIMIT = 100000

export const METAMASK_HOME_PAGE = 'https://metamask.io/'
11 changes: 11 additions & 0 deletions src/hooks/useInterval.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect } from 'react'

export const useInterval = (cb: () => void, delay: number) => {
useEffect(() => {
const id = setInterval(cb, delay)

return () => {
clearInterval(id)
}
})
}
8 changes: 3 additions & 5 deletions src/pages/Wrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ export const Wrapper: FC = () => {
addTokenToWallet,
} = useWeb3()
const {
state: { isLoading, balance, wRoseBalance, formType },
state: { isLoading, balance, wRoseBalance, formType, estimatedFee },
init,
setAmount,
getFeeAmount,
} = useWrapForm()

useEffect(() => {
Expand All @@ -52,10 +51,9 @@ export const Wrapper: FC = () => {
const handlePercentageCalc = (percentage: BigNumber) => {
if (formType === WrapFormType.WRAP) {
if (percentage.eq(100)) {
/* In case of 100% WRAP, deduct hardcoded gas fee */
/* In case of 100% WRAP, deduct gas fee */
const percAmount = NumberUtils.getPercentageAmount(balance, percentage)
const fee = getFeeAmount()
setAmount(percAmount.sub(fee))
setAmount(percAmount.sub(estimatedFee))
} else {
setAmount(NumberUtils.getPercentageAmount(balance, percentage))
}
Expand Down
30 changes: 20 additions & 10 deletions src/providers/Web3Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { createContext, FC, PropsWithChildren, useCallback, useState } from 'react'
import { BigNumber, ethers, utils } from 'ethers'
import * as sapphire from '@oasisprotocol/sapphire-paratime'
import { NETWORKS } from '../constants/config'
import { MAX_GAS_LIMIT, NETWORKS } from '../constants/config'
// https://repo.sourcify.dev/contracts/full_match/23295/0xB759a0fbc1dA517aF257D5Cf039aB4D86dFB3b94/
// https://repo.sourcify.dev/contracts/full_match/23294/0x8Bc2B030b299964eEfb5e1e0b36991352E56D2D3/
import WrappedRoseMetadata from '../contracts/WrappedROSE.json'
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { MetaMaskError, UnknownNetworkError } from '../utils/errors'
import detectEthereumProvider from '@metamask/detect-provider'

const MAX_GAS_PRICE = utils.parseUnits('100', 'gwei').toNumber()
const MAX_GAS_LIMIT = 100000

declare global {
interface Window {
ethereum?: ethers.providers.ExternalProvider & ethers.providers.Web3Provider
Expand All @@ -31,15 +28,16 @@ interface Web3ProviderState {

interface Web3ProviderContext {
readonly state: Web3ProviderState
wrap: (amount: string) => Promise<TransactionResponse>
unwrap: (amount: string) => Promise<TransactionResponse>
wrap: (amount: string, gasPrice: BigNumber) => Promise<TransactionResponse>
unwrap: (amount: string, gasPrice: BigNumber) => Promise<TransactionResponse>
isMetaMaskInstalled: () => Promise<boolean>
connectWallet: () => Promise<void>
switchNetwork: () => Promise<void>
getBalance: () => Promise<BigNumber>
getBalanceOfWROSE: () => Promise<BigNumber>
getTransaction: (txHash: string) => Promise<TransactionResponse>
addTokenToWallet: () => Promise<void>
getGasPrice: () => Promise<BigNumber>
}

const web3ProviderInitialState: Web3ProviderState = {
Expand Down Expand Up @@ -252,7 +250,18 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
}
}

const wrap = async (amount: string) => {
const getGasPrice = async () => {
const { sapphireEthProvider } = state

if (!sapphireEthProvider) {
// Silently fail
return BigNumber.from(0)
}

return await sapphireEthProvider.getGasPrice()
}

const wrap = async (amount: string, gasPrice: BigNumber) => {
if (!amount) {
throw new Error('[amount] is required!')
}
Expand All @@ -263,10 +272,10 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
throw new Error('[wRoseContract] not initialized!')
}

return await wRoseContract.deposit({ value: amount, gasLimit: MAX_GAS_LIMIT, gasPrice: MAX_GAS_PRICE })
return await wRoseContract.deposit({ value: amount, gasLimit: MAX_GAS_LIMIT, gasPrice })
}

const unwrap = async (amount: string) => {
const unwrap = async (amount: string, gasPrice: BigNumber) => {
if (!amount) {
throw new Error('[amount] is required!')
}
Expand All @@ -277,7 +286,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
throw new Error('[wRoseContract] not initialized!')
}

return await wRoseContract.withdraw(amount, { gasLimit: MAX_GAS_LIMIT, gasPrice: MAX_GAS_PRICE })
return await wRoseContract.withdraw(amount, { gasLimit: MAX_GAS_LIMIT, gasPrice })
}

const getTransaction = async (txHash: string) => {
Expand Down Expand Up @@ -331,6 +340,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
getBalanceOfWROSE,
getTransaction,
addTokenToWallet,
getGasPrice,
}

return <Web3Context.Provider value={providerState}>{children}</Web3Context.Provider>
Expand Down
60 changes: 48 additions & 12 deletions src/providers/WrapFormProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { createContext, FC, PropsWithChildren, useState } from 'react'
import { BigNumber, BigNumberish, utils } from 'ethers'
import { BigNumber, BigNumberish } from 'ethers'
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { useWeb3 } from '../hooks/useWeb3'
import { WrapFormType } from '../utils/types'
import { MAX_GAS_LIMIT } from '../constants/config'

interface WrapFormProviderState {
isLoading: boolean
amount: BigNumber | null
formType: WrapFormType
balance: BigNumber
wRoseBalance: BigNumber
estimatedFee: BigNumber
estimatedGasPrice: BigNumber
}

interface WrapFormProviderContext {
Expand All @@ -18,7 +21,8 @@ interface WrapFormProviderContext {
setAmount: (amount: BigNumberish) => void
toggleFormType: (amount: BigNumber | null) => void
submit: (amount: BigNumber) => Promise<TransactionResponse>
getFeeAmount: () => BigNumber
setFeeAmount: () => Promise<void>
debounceLeadingSetFeeAmount: (fn?: () => Promise<void>, timeout?: number) => () => void
}

const wrapFormProviderInitialState: WrapFormProviderState = {
Expand All @@ -27,6 +31,8 @@ const wrapFormProviderInitialState: WrapFormProviderState = {
formType: WrapFormType.UNWRAP,
balance: BigNumber.from(0),
wRoseBalance: BigNumber.from(0),
estimatedFee: BigNumber.from(0),
estimatedGasPrice: BigNumber.from(0),
}

export const WrapFormContext = createContext<WrapFormProviderContext>({} as WrapFormProviderContext)
Expand All @@ -38,6 +44,7 @@ export const WrapFormContextProvider: FC<PropsWithChildren> = ({ children }) =>
getBalanceOfWROSE,
wrap,
unwrap,
getGasPrice,
} = useWeb3()
const [state, setState] = useState<WrapFormProviderState>({
...wrapFormProviderInitialState,
Expand Down Expand Up @@ -83,19 +90,47 @@ export const WrapFormContextProvider: FC<PropsWithChildren> = ({ children }) =>
}
}

const getFeeAmount = () => {
return utils.parseUnits('0.01', 'ether')
const setFeeAmount = async () => {
const estimatedGasPrice = await getGasPrice()
const estimatedFee = estimatedGasPrice.mul(MAX_GAS_LIMIT)

setState(prevState => ({
...prevState,
estimatedGasPrice,
estimatedFee,
}))
}

/**
* Prevent spamming of fee estimation calculations
* @param fn
* @param timeout
*/
const debounceLeadingSetFeeAmount = (fn = setFeeAmount, timeout = 8000) => {
let id: ReturnType<typeof setTimeout> | null = null
return () => {
if (!id) {
fn()
}

if (id) {
clearTimeout(id)
}
id = setTimeout(() => {
id = null
}, timeout)
}
}

const toggleFormType = (amount: BigNumber | null) => {
const { balance, wRoseBalance, formType } = state
const toggleFormType = (amount: BigNumber | null): void => {
const { balance, wRoseBalance, formType, estimatedFee } = state

const toggledFormType = formType === WrapFormType.WRAP ? WrapFormType.UNWRAP : WrapFormType.WRAP

let maxAmount = amount

if (toggledFormType === WrapFormType.WRAP && amount?.gt(balance)) {
maxAmount = balance.sub(getFeeAmount())
maxAmount = balance.sub(estimatedFee)
} else if (toggledFormType === WrapFormType.UNWRAP && amount?.gt(wRoseBalance)) {
maxAmount = wRoseBalance
}
Expand All @@ -114,18 +149,18 @@ export const WrapFormContextProvider: FC<PropsWithChildren> = ({ children }) =>

_setIsLoading(true)

const { formType, balance, wRoseBalance } = state
const { formType, balance, wRoseBalance, estimatedFee, estimatedGasPrice } = state

let receipt: TransactionResponse | null = null

if (formType === WrapFormType.WRAP) {
if (amount.gt(balance.sub(getFeeAmount()))) {
if (amount.gt(balance.sub(estimatedFee))) {
_setIsLoading(false)
return Promise.reject(new Error('Insufficient balance'))
}

try {
receipt = await wrap(amount.toString())
receipt = await wrap(amount.toString(), estimatedGasPrice)
} catch (ex) {
_setIsLoading(false)
throw ex
Expand All @@ -137,7 +172,7 @@ export const WrapFormContextProvider: FC<PropsWithChildren> = ({ children }) =>
}

try {
receipt = await unwrap(amount.toString())
receipt = await unwrap(amount.toString(), estimatedGasPrice)
} catch (ex) {
_setIsLoading(false)
throw ex
Expand All @@ -154,7 +189,8 @@ export const WrapFormContextProvider: FC<PropsWithChildren> = ({ children }) =>
const providerState: WrapFormProviderContext = {
state,
init,
getFeeAmount,
setFeeAmount,
debounceLeadingSetFeeAmount,
setAmount,
toggleFormType,
submit,
Expand Down
11 changes: 10 additions & 1 deletion src/utils/number.utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { BigNumber } from 'ethers'
import { BigNumber, utils } from 'ethers'

export abstract class NumberUtils {
static getPercentageAmount(amount: BigNumber, percentage: BigNumber) {
return amount.mul(percentage).div(100)
}

/**
* Helper to round eth amount to 4 decimals
* @param amount
*/
static getTruncatedAmount(amount: BigNumber) {
const remainder = amount.mod(1e14)
return utils.formatEther(amount.sub(remainder))
}

// Compatible with https://github.com/MetaMask/metamask-extension/blob/v10.7.0/ui/helpers/utils/icon-factory.js#L84-L88
static jsNumberForAddress(address: string) {
const addr = address.slice(2, 10)
Expand Down

0 comments on commit 5e8cacc

Please sign in to comment.