github.com/ethereum-optimism/optimism@v1.7.2/packages/fee-estimation/src/estimateFees.ts (about) 1 import { 2 gasPriceOracleABI, 3 gasPriceOracleAddress, 4 } from '@eth-optimism/contracts-ts' 5 import { 6 getContract, 7 createPublicClient, 8 http, 9 BlockTag, 10 Address, 11 EstimateGasParameters, 12 serializeTransaction, 13 encodeFunctionData, 14 EncodeFunctionDataParameters, 15 TransactionSerializableEIP1559, 16 TransactionSerializedEIP1559, 17 PublicClient, 18 } from 'viem' 19 import * as chains from 'viem/chains' 20 import { Abi } from 'abitype' 21 22 /** 23 * Bytes type representing a hex string with a 0x prefix 24 * @typedef {`0x${string}`} Bytes 25 */ 26 export type Bytes = `0x${string}` 27 28 /** 29 * Options to query a specific block 30 */ 31 type BlockOptions = { 32 /** 33 * Block number to query from 34 */ 35 blockNumber?: bigint 36 /** 37 * Block tag to query from 38 */ 39 blockTag?: BlockTag 40 } 41 42 const knownChains = [ 43 chains.optimism.id, 44 chains.goerli.id, 45 chains.base, 46 chains.baseGoerli.id, 47 chains.zora, 48 chains.zoraTestnet, 49 ] 50 51 /** 52 * ClientOptions type 53 * @typedef {Object} ClientOptions 54 * @property {keyof typeof gasPriceOracleAddress | number} chainId - Chain ID 55 * @property {string} [rpcUrl] - RPC URL. If not provided the provider will attempt to use public RPC URLs for the chain 56 * @property {chains.Chain['nativeCurrency']} [nativeCurrency] - Native currency. Defaults to ETH 57 */ 58 type ClientOptions = 59 // for known chains like base don't require an rpcUrl 60 | { 61 chainId: (typeof knownChains)[number] 62 rpcUrl?: string 63 nativeCurrency?: chains.Chain['nativeCurrency'] 64 } 65 | { 66 chainId: number 67 rpcUrl: string 68 nativeCurrency?: chains.Chain['nativeCurrency'] 69 } 70 | PublicClient 71 72 /** 73 * Options for all GasPriceOracle methods 74 */ 75 export type GasPriceOracleOptions = BlockOptions & { client: ClientOptions } 76 77 /** 78 * Options for specifying the transaction being estimated 79 */ 80 export type OracleTransactionParameters< 81 TAbi extends Abi | readonly unknown[], 82 TFunctionName extends string | undefined = undefined 83 > = EncodeFunctionDataParameters<TAbi, TFunctionName> & 84 Omit<TransactionSerializableEIP1559, 'data' | 'type'> 85 /** 86 * Options for specifying the transaction being estimated 87 */ 88 export type GasPriceOracleEstimator = < 89 TAbi extends Abi | readonly unknown[], 90 TFunctionName extends string | undefined = undefined 91 >( 92 options: OracleTransactionParameters<TAbi, TFunctionName> & 93 GasPriceOracleOptions 94 ) => Promise<bigint> 95 96 /** 97 * Throws an error if fetch is not defined 98 * Viem requires fetch 99 */ 100 const validateFetch = (): void => { 101 if (typeof fetch === 'undefined') { 102 throw new Error( 103 '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' 104 ) 105 } 106 } 107 108 /** 109 * Internal helper to serialize a transaction 110 */ 111 const transactionSerializer = < 112 TAbi extends Abi | readonly unknown[], 113 TFunctionName extends string | undefined = undefined 114 >( 115 options: EncodeFunctionDataParameters<TAbi, TFunctionName> & 116 Omit<TransactionSerializableEIP1559, 'data'> 117 ): TransactionSerializedEIP1559 => { 118 const encodedFunctionData = encodeFunctionData(options) 119 const serializedTransaction = serializeTransaction({ 120 ...options, 121 data: encodedFunctionData, 122 type: 'eip1559', 123 }) 124 return serializedTransaction as TransactionSerializedEIP1559 125 } 126 127 /** 128 * Gets L2 client 129 * @example 130 * const client = getL2Client({ chainId: 1, rpcUrl: "http://localhost:8545" }); 131 */ 132 export const getL2Client = (options: ClientOptions): PublicClient => { 133 validateFetch() 134 135 if ('chainId' in options && options.chainId) { 136 const viemChain = Object.values(chains)?.find( 137 (chain) => chain.id === options.chainId 138 ) 139 const rpcUrls = options.rpcUrl 140 ? { default: { http: [options.rpcUrl] } } 141 : viemChain?.rpcUrls 142 if (!rpcUrls) { 143 throw new Error( 144 `No rpcUrls found for chainId ${options.chainId}. Please explicitly provide one` 145 ) 146 } 147 return createPublicClient({ 148 chain: { 149 id: options.chainId, 150 name: viemChain?.name ?? 'op-chain', 151 nativeCurrency: 152 options.nativeCurrency ?? 153 viemChain?.nativeCurrency ?? 154 chains.optimism.nativeCurrency, 155 network: viemChain?.network ?? 'Unknown OP Chain', 156 rpcUrls, 157 explorers: 158 (viemChain as typeof chains.optimism)?.blockExplorers ?? 159 chains.optimism.blockExplorers, 160 }, 161 transport: http( 162 options.rpcUrl ?? chains[options.chainId].rpcUrls.public.http[0] 163 ), 164 }) 165 } 166 return options as PublicClient 167 } 168 169 /** 170 * Get gas price Oracle contract 171 */ 172 export const getGasPriceOracleContract = (params: ClientOptions) => { 173 return getContract({ 174 address: gasPriceOracleAddress['420'], 175 abi: gasPriceOracleABI, 176 publicClient: getL2Client(params), 177 }) 178 } 179 180 /** 181 * Returns the base fee 182 * @returns {Promise<bigint>} - The base fee 183 * @example 184 * const baseFeeValue = await baseFee(params); 185 */ 186 export const baseFee = async ({ 187 client, 188 blockNumber, 189 blockTag, 190 }: GasPriceOracleOptions): Promise<bigint> => { 191 const contract = getGasPriceOracleContract(client) 192 return contract.read.baseFee({ blockNumber, blockTag }) 193 } 194 195 /** 196 * Returns the decimals used in the scalar 197 * @example 198 * const decimalsValue = await decimals(params); 199 */ 200 export const decimals = async ({ 201 client, 202 blockNumber, 203 blockTag, 204 }: GasPriceOracleOptions): Promise<bigint> => { 205 const contract = getGasPriceOracleContract(client) 206 return contract.read.decimals({ blockNumber, blockTag }) 207 } 208 209 /** 210 * Returns the gas price 211 * @example 212 * const gasPriceValue = await gasPrice(params); 213 */ 214 export const gasPrice = async ({ 215 client, 216 blockNumber, 217 blockTag, 218 }: GasPriceOracleOptions): Promise<bigint> => { 219 const contract = getGasPriceOracleContract(client) 220 return contract.read.gasPrice({ blockNumber, blockTag }) 221 } 222 223 /** 224 * Computes the L1 portion of the fee based on the size of the rlp encoded input 225 * transaction, the current L1 base fee, and the various dynamic parameters. 226 * @example 227 * const L1FeeValue = await getL1Fee(data, params); 228 */ 229 export const getL1Fee: GasPriceOracleEstimator = async (options) => { 230 const data = transactionSerializer(options) 231 const contract = getGasPriceOracleContract(options.client) 232 return contract.read.getL1Fee([data], { 233 blockNumber: options.blockNumber, 234 blockTag: options.blockTag, 235 }) 236 } 237 238 /** 239 * Returns the L1 gas used 240 * @example 241 */ 242 export const getL1GasUsed: GasPriceOracleEstimator = async (options) => { 243 const data = transactionSerializer(options) 244 const contract = getGasPriceOracleContract(options.client) 245 return contract.read.getL1GasUsed([data], { 246 blockNumber: options.blockNumber, 247 blockTag: options.blockTag, 248 }) 249 } 250 251 /** 252 * Returns the L1 base fee 253 * @example 254 * const L1BaseFeeValue = await l1BaseFee(params); 255 */ 256 export const l1BaseFee = async ({ 257 client, 258 blockNumber, 259 blockTag, 260 }: GasPriceOracleOptions): Promise<bigint> => { 261 const contract = getGasPriceOracleContract(client) 262 return contract.read.l1BaseFee({ blockNumber, blockTag }) 263 } 264 265 /** 266 * Returns the overhead 267 * @example 268 * const overheadValue = await overhead(params); 269 */ 270 export const overhead = async ({ 271 client, 272 blockNumber, 273 blockTag, 274 }: GasPriceOracleOptions): Promise<bigint> => { 275 const contract = getGasPriceOracleContract(client) 276 return contract.read.overhead({ blockNumber, blockTag }) 277 } 278 279 /** 280 * Returns the current fee scalar 281 * @example 282 * const scalarValue = await scalar(params); 283 */ 284 export const scalar = async ({ 285 client, 286 ...params 287 }: GasPriceOracleOptions): Promise<bigint> => { 288 const contract = getGasPriceOracleContract(client) 289 return contract.read.scalar(params) 290 } 291 292 /** 293 * Returns the version 294 * @example 295 * const versionValue = await version(params); 296 */ 297 export const version = async ({ 298 client, 299 ...params 300 }: GasPriceOracleOptions): Promise<string> => { 301 const contract = getGasPriceOracleContract(client) 302 return contract.read.version(params) 303 } 304 305 export type EstimateFeeParams = { 306 /** 307 * The transaction call data as a 0x-prefixed hex string 308 */ 309 data: Bytes 310 /** 311 * The address of the account that will be sending the transaction 312 */ 313 account: Address 314 } & GasPriceOracleOptions & 315 Omit<EstimateGasParameters, 'data' | 'account'> 316 317 export type EstimateFees = < 318 TAbi extends Abi | readonly unknown[], 319 TFunctionName extends string | undefined = undefined 320 >( 321 options: OracleTransactionParameters<TAbi, TFunctionName> & 322 GasPriceOracleOptions & 323 Omit<EstimateGasParameters, 'data'> 324 ) => Promise<bigint> 325 /** 326 * Estimates gas for an L2 transaction including the l1 fee 327 */ 328 export const estimateFees: EstimateFees = async (options) => { 329 const client = getL2Client(options.client) 330 const encodedFunctionData = encodeFunctionData({ 331 abi: options.abi, 332 args: options.args, 333 functionName: options.functionName, 334 } as EncodeFunctionDataParameters) 335 const [l1Fee, l2Gas, l2GasPrice] = await Promise.all([ 336 getL1Fee({ 337 ...options, 338 // account must be undefined or else viem will return undefined 339 account: undefined as any, 340 }), 341 client.estimateGas({ 342 to: options.to, 343 account: options.account, 344 accessList: options.accessList, 345 blockNumber: options.blockNumber, 346 blockTag: options.blockTag, 347 data: encodedFunctionData, 348 value: options.value, 349 } as EstimateGasParameters<typeof chains.optimism>), 350 client.getGasPrice(), 351 ]) 352 return l1Fee + l2Gas * l2GasPrice 353 }