Skip to content

Commit

Permalink
Merge pull request #18 from oasisprotocol/ml/estimate-gas-price-via-p…
Browse files Browse the repository at this point in the history
…rovider

Estimate gas price via provider
  • Loading branch information
lubej authored Jan 7, 2024
2 parents 157d7c3 + 4108ed7 commit f4c80a6
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 83 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/'
18 changes: 18 additions & 0 deletions src/hooks/useInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'

export const useInterval = (cb: () => void, delay: number) => {
const cbRef = useRef(() => {
})

useEffect(() => {
cbRef.current = cb
}, [cb])

useEffect(() => {
const id = setInterval(() => cbRef.current(), delay)

return () => {
clearInterval(id)
}
}, [delay])
}
2 changes: 1 addition & 1 deletion src/hooks/useWeb3.tsx → src/hooks/useWeb3.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react'
import { Web3Context } from '../providers/Web3Provider'
import { Web3Context } from '../providers/Web3Context'

export const useWeb3 = () => {
const value = useContext(Web3Context)
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useWrapForm.tsx → src/hooks/useWrapForm.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext } from 'react'
import { WrapFormContext } from '../providers/WrapFormProvider'
import { WrapFormContext } from '../providers/WrapFormContext'

export const useWrapForm = () => {
const value = useContext(WrapFormContext)
Expand Down
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
31 changes: 31 additions & 0 deletions src/providers/Web3Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext } from 'react'
import { BigNumber, ethers } from 'ethers'
import * as sapphire from '@oasisprotocol/sapphire-paratime'
import { TransactionResponse } from '@ethersproject/abstract-provider'

export interface Web3ProviderState {
isConnected: boolean
ethProvider: ethers.providers.Web3Provider | null
sapphireEthProvider: (ethers.providers.Web3Provider & sapphire.SapphireAnnex) | null
wRoseContractAddress: string | null
wRoseContract: ethers.Contract | null
account: string | null
explorerBaseUrl: string | null
networkName: string | null
}

export interface Web3ProviderContext {
readonly state: Web3ProviderState
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>
}

export const Web3Context = createContext<Web3ProviderContext>({} as Web3ProviderContext)
55 changes: 19 additions & 36 deletions src/providers/Web3Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,20 @@
import { createContext, FC, PropsWithChildren, useCallback, useState } from 'react'
import { 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
import { Web3ProviderContext, Web3ProviderState, Web3Context } from './Web3Context'

declare global {
interface Window {
ethereum?: ethers.providers.ExternalProvider & ethers.providers.Web3Provider
}
}

interface Web3ProviderState {
isConnected: boolean
ethProvider: ethers.providers.Web3Provider | null
sapphireEthProvider: (ethers.providers.Web3Provider & sapphire.SapphireAnnex) | null
wRoseContractAddress: string | null
wRoseContract: ethers.Contract | null
account: string | null
explorerBaseUrl: string | null
networkName: string | null
}

interface Web3ProviderContext {
readonly state: Web3ProviderState
wrap: (amount: string) => Promise<TransactionResponse>
unwrap: (amount: string) => 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>
}

const web3ProviderInitialState: Web3ProviderState = {
isConnected: false,
ethProvider: null,
Expand All @@ -53,8 +26,6 @@ const web3ProviderInitialState: Web3ProviderState = {
networkName: null,
}

export const Web3Context = createContext<Web3ProviderContext>({} as Web3ProviderContext)

export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
const [state, setState] = useState<Web3ProviderState>({
...web3ProviderInitialState,
Expand Down Expand Up @@ -252,7 +223,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 +245,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 +259,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 +313,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
getBalanceOfWROSE,
getTransaction,
addTokenToWallet,
getGasPrice,
}

return <Web3Context.Provider value={providerState}>{children}</Web3Context.Provider>
Expand Down
26 changes: 26 additions & 0 deletions src/providers/WrapFormContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createContext } from 'react'
import { BigNumber, BigNumberish } from 'ethers'
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { WrapFormType } from '../utils/types'

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

export interface WrapFormProviderContext {
readonly state: WrapFormProviderState
init: () => void
setAmount: (amount: BigNumberish) => void
toggleFormType: (amount: BigNumber | null) => void
submit: (amount: BigNumber) => Promise<TransactionResponse>
setFeeAmount: () => Promise<void>
debounceLeadingSetFeeAmount: (fn?: () => Promise<void>, timeout?: number) => () => void
}

export const WrapFormContext = createContext<WrapFormProviderContext>({} as WrapFormProviderContext)
Loading

0 comments on commit f4c80a6

Please sign in to comment.