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  }