Commit 5a817f1a authored by Will Cory's avatar Will Cory

feat: Add gas estimation utilities

feat: Add feeEstimation function and move to correct package

Apply suggestions from code review
Co-authored-by: default avatarAnnie Ke <annieke8@gmail.com>

fix: Remove the copy pasta from package.json

Update packages/fee-estimation/README.md

feat: Offer passing in viem client as an option

better api and tests

better docs

all the docs

fix: Make viem a peer dep

typo

fix: Refactor to use viem correctly (docs not updated yet)

moar tests more explicit implementations

Update packages/fee-estimation/src/estimateFees.ts

fix: lint

fix: Remove bad import and debugging fs.writeFile

chore: pnpm up --latest packages

feat: Add zora and base mainnet

fix: linter
parent 8698fc52
......@@ -54,14 +54,14 @@
"@vitest/coverage-istanbul": "^0.33.0",
"@wagmi/cli": "^1.3.0",
"@wagmi/core": "^1.3.8",
"abitype": "^0.9.2",
"abitype": "^0.9.3",
"glob": "^10.3.3",
"isomorphic-fetch": "^3.0.0",
"jest-dom": "link:@types/@testing-library/jest-dom",
"jsdom": "^22.1.0",
"tsup": "^7.1.0",
"typescript": "^5.1.6",
"vite": "^4.4.4",
"vite": "^4.4.6",
"vitest": "^0.33.0"
},
"peerDependencies": {
......@@ -80,6 +80,6 @@
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viem": "^1.3.0"
"viem": "^1.3.1"
}
}
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:03 PM
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:52 PM
import {
getContract,
GetContractArgs,
......
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:02 PM
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:50 PM
/* eslint-disable */
......
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:04 PM
// Generated by @wagmi/cli@1.3.0 on 7/17/2023 at 7:42:52 PM
import {
useNetwork,
useContractRead,
......
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
coverage
artifacts
cache
typechain
.deps
.envrc
.env
/dist/
module.exports = {
...require('../../.prettierrc.js'),
}
MIT License
Copyright (c) 2022 Optimism
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This diff is collapsed.
VITE_RPC_URL_L2_GOERLI=
VITE_RPC_URL_L2_MAINNET=
VITE_RPC_URL_L1_GOERLI=
VITE_RPC_URL_L1_MAINNET=
{
"name": "@eth-optimism/fee-estimation",
"version": "0.15.0",
"description": "Lightweight library for doing OP-Chain gas estimation",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git",
"directory": "packages/fee-estimation"
},
"homepage": "https://optimism.io",
"type": "module",
"main": "dist/estimateFees.js",
"module": "dist/estimateFees.mjs",
"types": "src/estimateFees.ts",
"files": [
"dist/",
"src/"
],
"scripts": {
"build": "tsup",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@eth-optimism/contracts-ts": "workspace:^",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react-hooks": "^8.0.1",
"@vitest/coverage-istanbul": "^0.33.0",
"abitype": "^0.9.3",
"isomorphic-fetch": "^3.0.0",
"jest-dom": "link:@types/@testing-library/jest-dom",
"jsdom": "^22.1.0",
"tsup": "^7.1.0",
"typescript": "^5.1.6",
"viem": "^1.3.1",
"vite": "^4.4.6",
"vitest": "^0.33.0"
},
"peerDependencies": {
"viem": "^0.3.30"
}
}
import fetch from 'isomorphic-fetch'
// viem needs this
global.fetch = fetch
/**
* The first 2 test cases are good documentation of how to use this library
*/
import { vi, test, expect, beforeEach } from 'vitest'
import { formatEther } from 'viem/utils'
import {
baseFee,
decimals,
estimateFees,
gasPrice,
getL1Fee,
getL1GasUsed,
getL2Client,
l1BaseFee,
overhead,
scalar,
version,
} from './estimateFees'
import {
optimistABI,
optimistAddress,
l2StandardBridgeABI,
l2StandardBridgeAddress,
} from '@eth-optimism/contracts-ts'
import { parseEther, parseGwei } from 'viem'
// using this optimist https://optimistic.etherscan.io/tx/0xaa291efba7ea40b0742e5ff84a1e7831a2eb6c2fc35001fa03ba80fd3b609dc9
const blockNumber = BigInt(107028270)
const optimistOwnerAddress =
'0x77194aa25a06f932c10c0f25090f3046af2c85a6' as const
const functionDataBurn = {
functionName: 'burn',
// this is an erc721 abi
abi: optimistABI,
args: [BigInt(optimistOwnerAddress)],
account: optimistOwnerAddress,
to: optimistAddress[10],
chainId: 10,
} as const
const functionDataBurnWithPriorityFees = {
...functionDataBurn,
maxFeePerGas: parseGwei('2'),
maxPriorityFeePerGas: parseGwei('2'),
} as const
// This tx
// https://optimistic.etherscan.io/tx/0xe6f3719be7327a991b9cb562ebf8d979cbca72bbdb2775f55a18274f4d0c9bbf
const functionDataWithdraw = {
abi: l2StandardBridgeABI,
functionName: 'withdraw',
value: BigInt(parseEther('0.00000001')),
account: '0x6387a88a199120aD52Dd9742C7430847d3cB2CD4',
// currently a bug is making chain id 10 not exist
to: l2StandardBridgeAddress[420],
chainId: 10,
args: [
// l2 token address
'0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000',
// amount
BigInt(parseEther('0.00000001')),
// l1 gas
0,
// extra data
'0x0',
],
maxFeePerGas: parseGwei('.2'),
maxPriorityFeePerGas: parseGwei('.1'),
} as const
const clientParams = {
chainId: functionDataBurn.chainId,
rpcUrl: process.env.VITE_L2_RPC_URL ?? 'https://mainnet.optimism.io',
} as const
const viemClient = getL2Client(clientParams)
const paramsWithRpcUrl = {
client: clientParams,
blockNumber,
} as const
const paramsWithViemClient = {
client: viemClient,
viemClient,
blockNumber,
} as const
const blockNumberWithdraw = BigInt(107046472)
const paramsWithRpcUrlWithdraw = {
client: clientParams,
blockNumber: blockNumberWithdraw,
} as const
beforeEach(() => {
vi.resetAllMocks()
})
test('estimateFees should return correct fees', async () => {
// burn
const res = await estimateFees({ ...paramsWithRpcUrl, ...functionDataBurn })
expect(res).toMatchInlineSnapshot('20573185261089n')
expect(formatEther(res)).toMatchInlineSnapshot('"0.000020573185261089"')
expect(
await estimateFees({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('20573185261089n')
expect(
await estimateFees({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('20573185261089n')
expect(
await estimateFees({
...paramsWithRpcUrl,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('21536974118090n')
// what is the l2 and l1 part of the fees for reference?
const l1Fee = await getL1Fee({ ...paramsWithRpcUrl, ...functionDataBurn })
const l2Fee = res - l1Fee
expect(l1Fee).toMatchInlineSnapshot('20573185216764n')
expect(formatEther(l1Fee)).toMatchInlineSnapshot('"0.000020573185216764"')
expect(l2Fee).toMatchInlineSnapshot('44325n')
expect(formatEther(l2Fee)).toMatchInlineSnapshot('"0.000000000000044325"')
// withdraw
const res2 = await estimateFees({
...paramsWithRpcUrlWithdraw,
...functionDataWithdraw,
})
expect(res2).toMatchInlineSnapshot('62857039016380n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857039016380n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857039016380n')
expect(
await estimateFees({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857039016380n')
expect(formatEther(res2)).toMatchInlineSnapshot('"0.00006285703901638"')
// what is the l2 and l1 part of the fees for reference?
const l1Fee2 = await getL1Fee({
...paramsWithRpcUrlWithdraw,
...functionDataWithdraw,
})
const l2Fee2 = res2 - l1Fee
expect(l1Fee2).toMatchInlineSnapshot('62857038894110n')
expect(formatEther(l1Fee2)).toMatchInlineSnapshot('"0.00006285703889411"')
expect(l2Fee2).toMatchInlineSnapshot('42283853799616n')
expect(formatEther(l2Fee2)).toMatchInlineSnapshot('"0.000042283853799616"')
})
test('baseFee should return the correct result', async () => {
expect(await baseFee(paramsWithRpcUrl)).toMatchInlineSnapshot('64n')
expect(await baseFee(paramsWithViemClient)).toMatchInlineSnapshot('64n')
})
test('decimals should return the correct result', async () => {
expect(await decimals(paramsWithRpcUrl)).toMatchInlineSnapshot('6n')
expect(await decimals(paramsWithViemClient)).toMatchInlineSnapshot('6n')
})
test('gasPrice should return the correct result', async () => {
expect(await gasPrice(paramsWithRpcUrl)).toMatchInlineSnapshot('64n')
expect(await gasPrice(paramsWithViemClient)).toMatchInlineSnapshot('64n')
})
test('getL1Fee should return the correct result', async () => {
// burn
expect(
await getL1Fee({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('20573185216764n')
expect(
await getL1Fee({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('20573185216764n')
expect(
await getL1Fee({
...paramsWithViemClient,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('21536974073765n')
expect(
formatEther(
await getL1Fee({ ...paramsWithViemClient, ...functionDataBurn })
)
).toMatchInlineSnapshot('"0.000020573185216764"')
// withdraw
expect(
await getL1Fee({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('62857038894110n')
expect(
formatEther(
await getL1Fee({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
)
).toMatchInlineSnapshot('"0.00006285703889411"')
})
test('getL1GasUsed should return the correct result', async () => {
// burn
expect(
await getL1GasUsed({ ...paramsWithRpcUrl, ...functionDataBurn })
).toMatchInlineSnapshot('2220n')
expect(
await getL1GasUsed({ ...paramsWithViemClient, ...functionDataBurn })
).toMatchInlineSnapshot('2220n')
expect(
await getL1GasUsed({
...paramsWithViemClient,
...functionDataBurnWithPriorityFees,
})
).toMatchInlineSnapshot('2324n')
// withdraw
expect(
await getL1GasUsed({ ...paramsWithRpcUrlWithdraw, ...functionDataWithdraw })
).toMatchInlineSnapshot('2868n')
})
test('l1BaseFee should return the correct result', async () => {
expect(await l1BaseFee(paramsWithRpcUrl)).toMatchInlineSnapshot(
'13548538813n'
)
expect(await l1BaseFee(paramsWithViemClient)).toMatchInlineSnapshot(
'13548538813n'
)
})
test('overhead should return the correct result', async () => {
expect(await overhead(paramsWithRpcUrl)).toMatchInlineSnapshot('188n')
expect(await overhead(paramsWithViemClient)).toMatchInlineSnapshot('188n')
})
test('scalar should return the correct result', async () => {
expect(await scalar(paramsWithRpcUrl)).toMatchInlineSnapshot('684000n')
expect(await scalar(paramsWithViemClient)).toMatchInlineSnapshot('684000n')
})
test('version should return the correct result', async () => {
expect(await version(paramsWithRpcUrl)).toMatchInlineSnapshot('"1.0.0"')
expect(await version(paramsWithViemClient)).toMatchInlineSnapshot('"1.0.0"')
})
import {
gasPriceOracleABI,
gasPriceOracleAddress,
} from '@eth-optimism/contracts-ts'
import {
getContract,
createPublicClient,
http,
BlockTag,
Address,
EstimateGasParameters,
serializeTransaction,
encodeFunctionData,
EncodeFunctionDataParameters,
TransactionSerializableEIP1559,
TransactionSerializedEIP1559,
PublicClient,
} from 'viem'
import * as chains from 'viem/chains'
import { Abi } from 'abitype'
/**
* Bytes type representing a hex string with a 0x prefix
* @typedef {`0x${string}`} Bytes
*/
export type Bytes = `0x${string}`
/**
* Options to query a specific block
*/
type BlockOptions = {
/**
* Block number to query from
*/
blockNumber?: bigint
/**
* Block tag to query from
*/
blockTag?: BlockTag
}
const knownChains = [
chains.optimism.id,
chains.goerli.id,
chains.base,
chains.baseGoerli.id,
chains.zora,
chains.zoraTestnet,
]
/**
* ClientOptions type
* @typedef {Object} ClientOptions
* @property {keyof typeof gasPriceOracleAddress | number} chainId - Chain ID
* @property {string} [rpcUrl] - RPC URL. If not provided the provider will attempt to use public RPC URLs for the chain
* @property {chains.Chain['nativeCurrency']} [nativeCurrency] - Native currency. Defaults to ETH
*/
type ClientOptions =
// for known chains like base don't require an rpcUrl
| {
chainId: typeof knownChains[number]
rpcUrl?: string
nativeCurrency?: chains.Chain['nativeCurrency']
}
| {
chainId: number
rpcUrl: string
nativeCurrency?: chains.Chain['nativeCurrency']
}
| PublicClient
/**
* Options for all GasPriceOracle methods
*/
export type GasPriceOracleOptions = BlockOptions & { client: ClientOptions }
/**
* Options for specifying the transaction being estimated
*/
export type OracleTransactionParameters<
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
> = EncodeFunctionDataParameters<TAbi, TFunctionName> &
Omit<TransactionSerializableEIP1559, 'data' | 'type'>
/**
* Options for specifying the transaction being estimated
*/
export type GasPriceOracleEstimator = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: OracleTransactionParameters<TAbi, TFunctionName> &
GasPriceOracleOptions
) => Promise<bigint>
/**
* Throws an error if fetch is not defined
* Viem requires fetch
*/
const validateFetch = (): void => {
if (typeof fetch === 'undefined') {
throw new Error(
'No fetch implementation found. Please provide a fetch polyfill. This can be done in NODE by passing in NODE_OPTIONS=--experimental-fetch or by using the isomorphic-fetch npm package'
)
}
}
/**
* Internal helper to serialize a transaction
*/
const transactionSerializer = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: EncodeFunctionDataParameters<TAbi, TFunctionName> &
Omit<TransactionSerializableEIP1559, 'data'>
): TransactionSerializedEIP1559 => {
const encodedFunctionData = encodeFunctionData(options)
const serializedTransaction = serializeTransaction({
...options,
data: encodedFunctionData,
type: 'eip1559',
})
return serializedTransaction as TransactionSerializedEIP1559
}
/**
* Gets L2 client
* @example
* const client = getL2Client({ chainId: 1, rpcUrl: "http://localhost:8545" });
*/
export const getL2Client = (options: ClientOptions): PublicClient => {
validateFetch()
if ('chainId' in options && options.chainId) {
const viemChain = Object.values(chains)?.find(
(chain) => chain.id === options.chainId
)
const rpcUrls = options.rpcUrl
? { default: { http: [options.rpcUrl] } }
: viemChain?.rpcUrls
if (!rpcUrls) {
throw new Error(
`No rpcUrls found for chainId ${options.chainId}. Please explicitly provide one`
)
}
return createPublicClient({
chain: {
id: options.chainId,
name: viemChain?.name ?? 'op-chain',
nativeCurrency:
options.nativeCurrency ??
viemChain?.nativeCurrency ??
chains.optimism.nativeCurrency,
network: viemChain?.network ?? 'Unknown OP Chain',
rpcUrls,
explorers:
(viemChain as typeof chains.optimism)?.blockExplorers ??
chains.optimism.blockExplorers,
},
transport: http(
options.rpcUrl ?? chains[options.chainId].rpcUrls.public.http[0]
),
})
}
return options as PublicClient
}
/**
* Get gas price Oracle contract
*/
export const getGasPriceOracleContract = (params: ClientOptions) => {
return getContract({
address: gasPriceOracleAddress['420'],
abi: gasPriceOracleABI,
publicClient: getL2Client(params),
})
}
/**
* Returns the base fee
* @returns {Promise<bigint>} - The base fee
* @example
* const baseFeeValue = await baseFee(params);
*/
export const baseFee = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.baseFee({ blockNumber, blockTag })
}
/**
* Returns the decimals used in the scalar
* @example
* const decimalsValue = await decimals(params);
*/
export const decimals = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.decimals({ blockNumber, blockTag })
}
/**
* Returns the gas price
* @example
* const gasPriceValue = await gasPrice(params);
*/
export const gasPrice = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.gasPrice({ blockNumber, blockTag })
}
/**
* Computes the L1 portion of the fee based on the size of the rlp encoded input
* transaction, the current L1 base fee, and the various dynamic parameters.
* @example
* const L1FeeValue = await getL1Fee(data, params);
*/
export const getL1Fee: GasPriceOracleEstimator = async (options) => {
const data = transactionSerializer(options)
const contract = getGasPriceOracleContract(options.client)
return contract.read.getL1Fee([data], {
blockNumber: options.blockNumber,
blockTag: options.blockTag,
})
}
/**
* Returns the L1 gas used
* @example
*/
export const getL1GasUsed: GasPriceOracleEstimator = async (options) => {
const data = transactionSerializer(options)
const contract = getGasPriceOracleContract(options.client)
return contract.read.getL1GasUsed([data], {
blockNumber: options.blockNumber,
blockTag: options.blockTag,
})
}
/**
* Returns the L1 base fee
* @example
* const L1BaseFeeValue = await l1BaseFee(params);
*/
export const l1BaseFee = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.l1BaseFee({ blockNumber, blockTag })
}
/**
* Returns the overhead
* @example
* const overheadValue = await overhead(params);
*/
export const overhead = async ({
client,
blockNumber,
blockTag,
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.overhead({ blockNumber, blockTag })
}
/**
* Returns the current fee scalar
* @example
* const scalarValue = await scalar(params);
*/
export const scalar = async ({
client,
...params
}: GasPriceOracleOptions): Promise<bigint> => {
const contract = getGasPriceOracleContract(client)
return contract.read.scalar(params)
}
/**
* Returns the version
* @example
* const versionValue = await version(params);
*/
export const version = async ({
client,
...params
}: GasPriceOracleOptions): Promise<string> => {
const contract = getGasPriceOracleContract(client)
return contract.read.version(params)
}
export type EstimateFeeParams = {
/**
* The transaction call data as a 0x-prefixed hex string
*/
data: Bytes
/**
* The address of the account that will be sending the transaction
*/
account: Address
} & GasPriceOracleOptions &
Omit<EstimateGasParameters, 'data' | 'account'>
export type EstimateFees = <
TAbi extends Abi | readonly unknown[],
TFunctionName extends string | undefined = undefined
>(
options: OracleTransactionParameters<TAbi, TFunctionName> &
GasPriceOracleOptions &
Omit<EstimateGasParameters, 'data'>
) => Promise<bigint>
/**
* Estimates gas for an L2 transaction including the l1 fee
*/
export const estimateFees: EstimateFees = async (options) => {
const client = getL2Client(options.client)
const encodedFunctionData = encodeFunctionData({
abi: options.abi,
args: options.args,
functionName: options.functionName,
} as EncodeFunctionDataParameters)
const [l1Fee, l2Fee] = await Promise.all([
getL1Fee({
...options,
// account must be undefined or else viem will return undefined
account: undefined as any,
}),
client.estimateGas({
to: options.to,
account: options.account,
accessList: options.accessList,
blockNumber: options.blockNumber,
blockTag: options.blockTag,
data: encodedFunctionData,
value: options.value,
} as EstimateGasParameters<typeof chains.optimism>),
])
return l1Fee + l2Fee
}
/// <reference types="vite/client" />
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./src",
"strict": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react",
"target": "ESNext",
"noEmit": true
},
"include": ["./src"]
}
import { defineConfig } from 'tsup'
import packageJson from './package.json'
// @see https://tsup.egoist.dev/
export default defineConfig({
name: packageJson.name,
entry: ['src/estimateFees.ts'],
outDir: 'dist',
format: ['esm', 'cjs'],
splitting: false,
sourcemap: true,
clean: false,
})
import { defineConfig } from 'vitest/config'
// @see https://vitest.dev/config/
export default defineConfig({
test: {
setupFiles: './setupVitest.ts',
environment: 'jsdom',
coverage: {
provider: 'istanbul',
},
},
})
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment