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 @@ ...@@ -54,14 +54,14 @@
"@vitest/coverage-istanbul": "^0.33.0", "@vitest/coverage-istanbul": "^0.33.0",
"@wagmi/cli": "^1.3.0", "@wagmi/cli": "^1.3.0",
"@wagmi/core": "^1.3.8", "@wagmi/core": "^1.3.8",
"abitype": "^0.9.2", "abitype": "^0.9.3",
"glob": "^10.3.3", "glob": "^10.3.3",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jest-dom": "link:@types/@testing-library/jest-dom", "jest-dom": "link:@types/@testing-library/jest-dom",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"tsup": "^7.1.0", "tsup": "^7.1.0",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^4.4.4", "vite": "^4.4.6",
"vitest": "^0.33.0" "vitest": "^0.33.0"
}, },
"peerDependencies": { "peerDependencies": {
...@@ -80,6 +80,6 @@ ...@@ -80,6 +80,6 @@
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^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 { import {
getContract, getContract,
GetContractArgs, 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 */ /* 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 { import {
useNetwork, useNetwork,
useContractRead, 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.
# @eth-optimism/fee-estimation
## Overview
This package is designed to provide an easy way to estimate gas on OP chains.
Fee estimation on OP-chains has both an l2 and l1 component. By default tools such as Viem, Wagmi, Ethers, and Web3.js do not support the l1 component. They will support this soon but in meantime, this library can help estimate fees for transactions, or act as a reference.
As these tools add support for gas estimation natively this README will be updated with framework specific instructions.
For more detailed information about gas fees on Optimism's Layer 2, you can visit their [official documentation](https://community.optimism.io/docs/developers/build/transaction-fees/#the-l2-execution-fee).
## GasPriceOracle contract
- The l2 contract that can estimate l1Fees is called [GasPriceOracle](../contracts-bedrock/contracts/l2/GasPriceOracle.sol) contract. This library provides utils for interacting with it at a high level.
- The GasPriceOracle is [deployed to Optimism](https://optimistic.etherscan.io/address/0x420000000000000000000000000000000000000F) and other OP chains at a predeployed address of `0x420000000000000000000000000000000000000F`
This library provides a higher level abstraction over the gasPriceOracle
## Installation
```bash
pnpm install @eth-optimism/fee-estimation
```
```bash
npm install @eth-optimism/fee-estimation
```
```bash
yarn add @eth-optimism/fee-estimation
```
## Usage
### Import the package
```ts
import { estimateFees } from '@eth-optimism/fee-estimation'
```
### Basic Usage
```ts
const fees = await estimateFees({
client: {
chainId: 10,
rpcUrl: 'https://mainnet.optimism.io',
},
blockNumber: BigInt(106889079),
account: '0xe371815c5f8a4f9acd1576879de288acd81269f1',
to: '0xe35f24470730f5a488da9721548c1ab0b65b53d5',
data: '0x5c19a95c00000000000000000000000046abfe1c972fca43766d6ad70e1c1df72f4bb4d1',
})
```
## API
### `estimateFees` function
```ts
estimateFees(params: EstimateFeeParams): Promise<bigint>
```
#### Parameters
`params`: An object with the following fields:
- `client`: An object with `rpcUrl` and `chainId` fields, or an instance of a Viem `PublicClient`.
- `blockNumber`: A BigInt representing the block number at which you want to estimate the fees.
- `blockTag`: A string representing the block tag to query from.
- `account`: A string representing the account making the transaction.
- `to`: A string representing the recipient of the transaction.
- `data`: A string representing the data being sent with the transaction. This should be a 0x-prefixed hex string.
#### Returns
A Promise that resolves to a BigInt representing the estimated fee in wei.
## FAQ
### How to encode function data?
You can use our package to encode the function data. Here is an example:
```ts
import { encodeFunctionData } from '@eth-optimism/fee-estimation'
import { optimistABI } from '@eth-optimism/contracts-ts'
const data = encodeFunctionData({
functionName: 'burn',
abi: optimistABI,
args: [BigInt('0x77194aa25a06f932c10c0f25090f3046af2c85a6')],
})
```
This will return a 0x-prefixed hex string that represents the encoded function data.
## Testing
The package provides a set of tests that you can run to verify its operation. The tests are a great resource for examples. You can find them at `./src/estimateFees.spec.ts`.
## Other methods
This package also provides lower level methods for estimating gas
### getL2Client()
This method returns a Layer 2 (L2) client that communicates with an L2 network.
```ts
getL2Client(options: ClientOptions): PublicClient;
```
#### Parameters
- `options: ClientOptions` - The options required to initialize the L2 client.
#### Returns
- `PublicClient` - Returns a public client that can interact with the L2 network.
#### Example
```ts
const clientParams = {
chainId: 10,
rpcUrl: process.env.VITE_L2_RPC_URL ?? 'https://mainnet.optimism.io',
} as const
const client = getL2Client(clientParams)
```
---
### baseFee()
Returns the base fee.
```ts
baseFee({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the base fee.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the base fee.
#### Example
```ts
const blockNumber = BigInt(106889079)
const paramsWithClient = {
client: clientParams,
blockNumber,
}
const baseFeeValue = await baseFee(paramsWithClient)
```
---
### decimals()
Returns the decimals used in the scalar.
```ts
decimals({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the decimals.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the decimals used in the scalar.
#### Example
```ts
const decimalsValue = await decimals(paramsWithClient)
```
---
### gasPrice()
Returns the gas price.
```ts
gasPrice({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - The options required to fetch the gas price.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the gas price.
#### Example
```ts
const gasPriceValue = await gasPrice(paramsWithClient)
```
---
### getL1Fee()
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.
```ts
getL1Fee(data: Bytes, { client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `data: Bytes` - The transaction call data as a 0x-prefixed hex string.
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the L1 portion of the fee.
#### Example
```ts
const data =
'0x5c19a95c00000000000000000000000046abfe1c972fca43766d6ad70e1c1df72f4bb4d1'
const l1FeeValue = await getL1Fee(data, paramsWithClient)
```
### getL1GasUsed()
This method returns the amount of gas used on the L1 network for a given transaction.
```ts
getL1GasUsed(data: Bytes, { client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `data: Bytes` - The transaction call data as a 0x-prefixed hex string.
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the amount of gas used on the L1 network for the given transaction.
#### Example
```ts
const data =
'0x5c19a95c00000000000000000000000046abfe1c972fca43766d6ad70e1c1df72f4bb4d1'
const l1GasUsed = await getL1GasUsed(data, paramsWithClient)
```
---
### l1BaseFee()
Returns the base fee on the L1 network.
```ts
l1BaseFee({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the base fee on the L1 network.
#### Example
```ts
const l1BaseFeeValue = await l1BaseFee(paramsWithClient)
```
---
### overhead()
Returns the overhead for the given transaction.
```ts
overhead({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the overhead for the given transaction.
#### Example
```ts
const overheadValue = await overhead(paramsWithClient)
```
---
### scalar()
Returns the scalar value for the gas estimation.
```ts
scalar({ client, ...params }: GasPriceOracleOptions): Promise<bigint>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the scalar value for the gas estimation.
#### Example
```ts
const scalarValue = await scalar(paramsWithClient)
```
---
### version()
Returns the version of the fee estimation library.
```ts
version({ client, ...params }: GasPriceOracleOptions): Promise<string>;
```
#### Parameters
- `{ client, ...params }: GasPriceOracleOptions` - Optional lock options and provider options.
#### Returns
- `Promise<string>` - Returns a promise that resolves to the version of the fee estimation library.
#### Example
```ts
const libraryVersion = await version(paramsWithClient)
```
---
### encodeFunctionData()
Encodes function data based on a given function name and arguments.
```ts
encodeFunctionData({ functionName, abi, args }: EncodeFunctionDataParams): string;
```
#### Parameters
- `{ functionName, abi, args }: EncodeFunctionDataParams` - An object containing the function name, ABI (Application Binary Interface), and arguments.
#### Returns
- `string` - Returns the encoded function data as a string.
#### Example
```ts
const encodedData = encodeFunctionData({
functionName: 'burn',
abi: optimistABI,
args: [BigInt(optimistOwnerAddress)],
})
```
---
### estimateFees()
Estimates the fee for a transaction given the input data and the address of the sender and recipient.
```ts
estimateFees({ client, data, account, to, blockNumber }: EstimateFeesParams): Promise<bigint>;
```
#### Parameters
- `{ client, data, account, to, blockNumber }: EstimateFeesParams` - An object containing the client, transaction data, sender's address, recipient's address, and block number.
#### Returns
- `Promise<bigint>` - Returns a promise that resolves to the estimated fee for the given transaction.
#### Example
```ts
const estimateFeesParams = {
data: '0xd1e16f0a603acf1f8150e020434b096e408bafa429a7134fbdad2ae82a9b2b882bfcf5fe174162cf4b3d5f2ab46ff6433792fc99885d55ce0972d982583cc1e11b64b1d8d50121c0497642000000000000000000000000000000000000060a2c8052ed420000000000000000000000000000000000004234002c8052edba12222222228d8ba445958a75a0704d566bf2c84200000000000000000000000000000000000006420000000000000000000000000000000000004239965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003',
account: '0xe371815c5f8a4f9acd1576879de288acd81269f1',
to: '0xe35f24470730f5a488da9721548c1ab0b65b53d5',
}
const estimatedFees = await estimateFees({
...paramsWithClient,
...estimateFeesParams,
})
```
I hope this information is helpful!
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