Skip to content

Commit

Permalink
feat: Add dynamic gas fee calculations (smlxl#76)
Browse files Browse the repository at this point in the history
- Add documentation for dynamic gas cost and gas refunds
- Add a gas estimator to compute fees based on Inputs and Common params
- Change "Static gas" column to "Minimum gas" to avoid misleading 0 cost
- Use Input stack variable names more consistently in the doc
- Extend opcodes.json with dynamicFee definition of user inputs
- Add support for opcode specific dynamic docs with variables from Common gasPrices param
- Show dynamic gas fee tooltip only when it's active in the selected fork

UI:
- Add focus state on the input fields

Misc:
- Upgrade to NextJS 12 with Webpack 5
- Get rid of browserify with global ethereumjs objects
- Simplify lookup of table's opcodes object
- Fix saving of selected fork setting value
- Fix setting common chain and hardfork

Closes smlxl#57 smlxl#67 smlxl#85
  • Loading branch information
Tair Asim authored Dec 24, 2021
1 parent a41de7f commit d96b26d
Show file tree
Hide file tree
Showing 215 changed files with 3,639 additions and 1,823 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
'plugin:prettier/recommended',
],
rules: {
curly: 'error',
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# misc
.DS_Store
*.pem
tsconfig.tsbuildinfo

# debug
npm-debug.log*
Expand Down
8 changes: 5 additions & 3 deletions components/ChainSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ const ChainSelector = () => {
(option: OnChangeValue<any, any>) => {
setForkValue(option)
onForkChange(option.value)
setSetting(Setting.VmFork, option)
setSetting(Setting.VmFork, option.value)
},
[onForkChange, setSetting],
)

useEffect(() => {
if (defaultForkOption) {
handleForkChange(getSetting(Setting.VmFork) || defaultForkOption)
if (settingsLoaded && defaultForkOption) {
const setting = getSetting(Setting.VmFork)
const storedFork = forkOptions.find((fork) => fork.value === setting)
handleForkChange(storedFork || defaultForkOption)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settingsLoaded, defaultForkOption])
Expand Down
8 changes: 6 additions & 2 deletions components/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ const Editor = ({ readOnly = false }: Props) => {
log('Solidity compiler loaded')

return () => {
if (solcWorkerRef?.current) solcWorkerRef.current.terminate()
if (solcWorkerRef?.current) {
solcWorkerRef.current.terminate()
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
Expand All @@ -130,7 +132,9 @@ const Editor = ({ readOnly = false }: Props) => {
}

const highlightCode = (value: string) => {
if (!codeType) return value
if (!codeType) {
return value
}

return codeHighlight(value, codeType)
.value.split('\n')
Expand Down
86 changes: 76 additions & 10 deletions components/Reference/DocRow.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { useContext, useMemo, useEffect, useState } from 'react'

import cn from 'classnames'
import { MDXRemote } from 'next-mdx-remote'
import { IOpcodeDoc } from 'types'
import { IOpcode, IOpcodeDoc, IOpcodeGasDoc } from 'types'

import { EthereumContext } from 'context/ethereumContext'

import { GITHUB_REPO_URL } from 'util/constants'
import { parseGasPrices, findMatchingForkName } from 'util/gas'

import * as Doc from 'components/ui/Doc'

import DynamicFee from './DynamicFee'

type Props = {
opcode: IOpcodeDoc
opcodeDoc: IOpcodeDoc
opcode: IOpcode
gasDocs: IOpcodeGasDoc
dynamicFeeForkName: string
}

const docComponents = {
Expand All @@ -21,32 +32,87 @@ const docComponents = {
th: Doc.TH,
td: Doc.TD,
a: Doc.A,
pre: Doc.Pre,
}

const DocRow = ({ opcode }: Props) => {
const API_DYNAMIC_FEE_DOC_URL = '/api/getDynamicDoc'

const DocRow = ({ opcodeDoc, opcode, gasDocs, dynamicFeeForkName }: Props) => {
const { common, forks, selectedFork } = useContext(EthereumContext)
const [dynamicFeeDocMdx, setDynamicFeeDocMdx] = useState()

const dynamicFeeDoc = useMemo(() => {
if (!gasDocs) {
return null
}
const fork = findMatchingForkName(forks, Object.keys(gasDocs), selectedFork)
return fork && common ? parseGasPrices(common, gasDocs[fork]) : null
}, [forks, selectedFork, gasDocs, common])

useEffect(() => {
let controller: AbortController | null = new AbortController()

const fetchDynamicFeeDoc = async () => {
try {
const response = await fetch(API_DYNAMIC_FEE_DOC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: dynamicFeeDoc }),
signal: controller?.signal,
})
const data = await response.json()
setDynamicFeeDocMdx(data.mdx)
controller = null
} catch (error) {
setDynamicFeeDocMdx(undefined)
}
}

if (dynamicFeeDoc) {
fetchDynamicFeeDoc()
}

return () => controller?.abort()
}, [dynamicFeeDoc])

return (
<div className="text-sm px-4 md:px-8 py-8 bg-indigo-50 dark:bg-black-600">
{opcode && (
{opcodeDoc && (
<>
<table className="table-auto mb-6 bg-indigo-100 dark:bg-black-500 rounded font-medium">
<thead>
<tr className="text-gray-500 uppercase text-xs">
<td className="pt-2 px-4">Since</td>
<td className="pt-2 px-4">Group</td>
<td className="pt-3 px-4">Since</td>
<td className="pt-3 px-4">Group</td>
</tr>
</thead>
<tbody>
<tr>
<td className="pb-2 px-4">{opcode.meta.fork}</td>
<td className="pb-2 px-4">{opcode.meta.group}</td>
<td className="pb-3 px-4">{opcodeDoc.meta.fork}</td>
<td className="pb-3 px-4">{opcodeDoc.meta.group}</td>
</tr>
</tbody>
</table>

<MDXRemote {...opcode.mdxSource} components={docComponents} />
<div className="flex flex-col lg:flex-row">
<div
className={cn({
'flex-1 lg:pr-8': opcode.dynamicFee,
})}
>
<MDXRemote {...opcodeDoc.mdxSource} components={docComponents} />
{dynamicFeeForkName && dynamicFeeDocMdx && (
<MDXRemote {...dynamicFeeDocMdx} components={docComponents} />
)}
</div>

{dynamicFeeForkName && (
<DynamicFee opcode={opcode} fork={dynamicFeeForkName} />
)}
</div>
</>
)}
{!opcode && (
{!opcodeDoc && (
<div>
There is no reference doc for this opcode yet. Why not{' '}
<a
Expand Down
123 changes: 123 additions & 0 deletions components/Reference/DynamicFee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useState, useContext } from 'react'

import debounce from 'lodash.debounce'
import { IOpcode } from 'types'

import { EthereumContext } from 'context/ethereumContext'

import { calculateDynamicFee } from 'util/gas'

import { Input, Radio, Icon } from 'components/ui'
import { H2 } from 'components/ui/Doc'

const debounceTimeout = 100 // ms

type Props = {
opcode: IOpcode
fork: string
}

type InputValue = {
[name: string]: string | undefined
}

const DynamicFee = ({ opcode, fork }: Props) => {
const { dynamicFee } = opcode
const forkInputs = dynamicFee ? dynamicFee[fork].inputs : null

const { common } = useContext(EthereumContext)
const [inputs, setInputs] = useState<InputValue | undefined>()
const [result, setResult] = useState('0')

const handleCompute = debounce((inputs) => {
if (common) {
try {
setResult(calculateDynamicFee(opcode, common, inputs))
} catch (error) {
console.error(error)
}
}
}, debounceTimeout)

// Initialize inputs with default keys & values
useEffect(() => {
const inputValues: InputValue = {}
Object.keys(forkInputs || []).map((key: string) => {
inputValues[key] = '0' // false for boolean, zero for numbers
})
setInputs(inputValues)
handleCompute(inputValues)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dynamicFee])

const handleChange = (key: string, value: string) => {
const newInputs = {
...inputs,
[key]: value,
}
setInputs(newInputs)
handleCompute(newInputs)
}

if (!forkInputs || !inputs) {
return null
}

return (
<div className="md:w-96">
<div className="flex justify-between items-center">
<H2>Estimate your gas cost</H2>
</div>

<div className="bg-indigo-100 dark:bg-black-500 p-4 rounded shadow">
{Object.keys(forkInputs).map((key: string) => {
const input = forkInputs[key]

return (
<div key={key}>
<label className="block text-sm text-gray-500 mb-1" htmlFor={key}>
{input.label}
</label>

{input.type === 'number' && (
<Input
type="number"
min={0}
max={1000000000000}
name={key}
value={inputs[key]}
onChange={(e) => handleChange(key, e.target.value)}
className="bg-white bg-opacity-75 dark:bg-black-400 mb-4 text-sm font-mono"
/>
)}

{input.type === 'boolean' && (
<div className="mb-4">
<Radio
text="Yes"
value={'1'}
isChecked={inputs[key] === '1'}
onChange={() => handleChange(key, '1')}
/>
<Radio
text="No"
value={'0'}
isChecked={inputs[key] === '0'}
onChange={() => handleChange(key, '0')}
/>
</div>
)}
</div>
)
})}

<div className="flex items-center pt-2">
<Icon name="gas-station-fill" className="text-indigo-500 mr-2" />
Static gas + dynamic gas = {result}
</div>
</div>
</div>
)
}

export default DynamicFee
1 change: 1 addition & 0 deletions components/Reference/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const Filters = ({ onSetFilter }: Props) => {
handleKeywordChange(e.target.value)
}}
placeholder={`Enter keyword...`}
className="bg-gray-100 dark:bg-black-500"
/>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions components/Reference/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const tableData = [
width: 128,
},
{
Header: 'Static Gas',
accessor: 'fee',
Header: 'Minimum Gas',
accessor: 'minimumFee',
width: 96,
},
{
Expand Down
Loading

0 comments on commit d96b26d

Please sign in to comment.