l2-provider.ts 8.44 KB
Newer Older
1 2 3 4 5 6
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import { serialize } from '@ethersproject/transactions'
import { Contract, BigNumber } from 'ethers'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import cloneDeep from 'lodash/cloneDeep'

7
import { assert } from './utils/assert'
8 9 10
import { L2Provider, ProviderLike, NumberLike } from './interfaces'
import { toProvider, toNumber, toBigNumber } from './utils'

11 12
type ProviderTypeIsWrong = any

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
/**
 * Gets a reasonable nonce for the transaction.
 *
 * @param provider Provider to get the nonce from.
 * @param tx Requested transaction.
 * @returns A reasonable nonce for the transaction.
 */
const getNonceForTx = async (
  provider: ProviderLike,
  tx: TransactionRequest
): Promise<number> => {
  if (tx.nonce !== undefined) {
    return toNumber(tx.nonce as NumberLike)
  } else if (tx.from !== undefined) {
    return toProvider(provider).getTransactionCount(tx.from)
  } else {
    // Large nonce with lots of non-zero bytes
    return 0xffffffff
  }
}

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
/**
 * Returns a Contract object for the GasPriceOracle.
 *
 * @param provider Provider to attach the contract to.
 * @returns Contract object for the GasPriceOracle.
 */
const connectGasPriceOracle = (provider: ProviderLike): Contract => {
  return new Contract(
    predeploys.OVM_GasPriceOracle,
    getContractInterface('OVM_GasPriceOracle'),
    toProvider(provider)
  )
}

/**
 * Gets the current L1 gas price as seen on L2.
 *
 * @param l2Provider L2 provider to query the L1 gas price from.
 * @returns Current L1 gas price as seen on L2.
 */
export const getL1GasPrice = async (
  l2Provider: ProviderLike
): Promise<BigNumber> => {
  const gpo = connectGasPriceOracle(l2Provider)
58
  return gpo.l1BaseFee()
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
}

/**
 * Estimates the amount of L1 gas required for a given L2 transaction.
 *
 * @param l2Provider L2 provider to query the gas usage from.
 * @param tx Transaction to estimate L1 gas for.
 * @returns Estimated L1 gas.
 */
export const estimateL1Gas = async (
  l2Provider: ProviderLike,
  tx: TransactionRequest
): Promise<BigNumber> => {
  const gpo = connectGasPriceOracle(l2Provider)
  return gpo.getL1GasUsed(
    serialize({
75 76 77 78 79
      data: tx.data,
      to: tx.to,
      gasPrice: tx.gasPrice,
      type: tx.type,
      gasLimit: tx.gasLimit,
80
      nonce: await getNonceForTx(l2Provider, tx),
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    })
  )
}

/**
 * Estimates the amount of L1 gas cost for a given L2 transaction in wei.
 *
 * @param l2Provider L2 provider to query the gas usage from.
 * @param tx Transaction to estimate L1 gas cost for.
 * @returns Estimated L1 gas cost.
 */
export const estimateL1GasCost = async (
  l2Provider: ProviderLike,
  tx: TransactionRequest
): Promise<BigNumber> => {
  const gpo = connectGasPriceOracle(l2Provider)
  return gpo.getL1Fee(
    serialize({
99 100 101 102 103
      data: tx.data,
      to: tx.to,
      gasPrice: tx.gasPrice,
      type: tx.type,
      gasLimit: tx.gasLimit,
104
      nonce: await getNonceForTx(l2Provider, tx),
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
    })
  )
}

/**
 * Estimates the L2 gas cost for a given L2 transaction in wei.
 *
 * @param l2Provider L2 provider to query the gas usage from.
 * @param tx Transaction to estimate L2 gas cost for.
 * @returns Estimated L2 gas cost.
 */
export const estimateL2GasCost = async (
  l2Provider: ProviderLike,
  tx: TransactionRequest
): Promise<BigNumber> => {
  const parsed = toProvider(l2Provider)
  const l2GasPrice = await parsed.getGasPrice()
  const l2GasCost = await parsed.estimateGas(tx)
  return l2GasPrice.mul(l2GasCost)
}

/**
 * Estimates the total gas cost for a given L2 transaction in wei.
 *
 * @param l2Provider L2 provider to query the gas usage from.
 * @param tx Transaction to estimate total gas cost for.
 * @returns Estimated total gas cost.
 */
export const estimateTotalGasCost = async (
  l2Provider: ProviderLike,
  tx: TransactionRequest
): Promise<BigNumber> => {
  const l1GasCost = await estimateL1GasCost(l2Provider, tx)
  const l2GasCost = await estimateL2GasCost(l2Provider, tx)
  return l1GasCost.add(l2GasCost)
}

142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
/**
 * Determines if a given Provider is an L2Provider.  Will coerce type
 * if true
 *
 * @param provider The provider to check
 * @returns Boolean
 * @example
 * if (isL2Provider(provider)) {
 *   // typescript now knows it is of type L2Provider
 *   const gasPrice = await provider.estimateL2GasPrice(tx)
 * }
 */
export const isL2Provider = <TProvider extends Provider>(
  provider: TProvider
): provider is L2Provider<TProvider> => {
  return Boolean((provider as L2Provider<TProvider>)._isL2Provider)
}

160 161 162 163 164 165 166 167 168 169 170 171
/**
 * Returns an provider wrapped as an Optimism L2 provider. Adds a few extra helper functions to
 * simplify the process of estimating the gas usage for a transaction on Optimism. Returns a COPY
 * of the original provider.
 *
 * @param provider Provider to wrap into an L2 provider.
 * @returns Provider wrapped as an L2 provider.
 */
export const asL2Provider = <TProvider extends Provider>(
  provider: TProvider
): L2Provider<TProvider> => {
  // Skip if we've already wrapped this provider.
172 173
  if (isL2Provider(provider)) {
    return provider
174 175
  }

176 177 178 179
  // Make a copy of the provider since we'll be modifying some internals and don't want to mess
  // with the original object.
  const l2Provider = cloneDeep(provider) as L2Provider<TProvider>

180 181 182
  // Not exactly sure when the provider wouldn't have a formatter function, but throw an error if
  // it doesn't have one. The Provider type doesn't define it but every provider I've dealt with
  // seems to have it.
183 184 185
  // TODO this may be fixed if library has gotten updated since
  const formatter = (l2Provider as ProviderTypeIsWrong).formatter
  assert(formatter, `provider.formatter must be defined`)
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263

  // Modify the block formatter to return the state root. Not strictly related to Optimism, just a
  // generally useful thing that really should've been on the Ethers block object to begin with.
  // TODO: Maybe we should make a PR to add this to the Ethers library?
  const ogBlockFormatter = formatter.block.bind(formatter)
  formatter.block = (block: any) => {
    const parsed = ogBlockFormatter(block)
    parsed.stateRoot = block.stateRoot
    return parsed
  }

  // Modify the block formatter to include all the L2 fields for transactions.
  const ogBlockWithTxFormatter = formatter.blockWithTransactions.bind(formatter)
  formatter.blockWithTransactions = (block: any) => {
    const parsed = ogBlockWithTxFormatter(block)
    parsed.stateRoot = block.stateRoot
    parsed.transactions = parsed.transactions.map((tx: any, idx: number) => {
      const ogTx = block.transactions[idx]
      tx.l1BlockNumber = ogTx.l1BlockNumber
        ? toNumber(ogTx.l1BlockNumber)
        : ogTx.l1BlockNumber
      tx.l1Timestamp = ogTx.l1Timestamp
        ? toNumber(ogTx.l1Timestamp)
        : ogTx.l1Timestamp
      tx.l1TxOrigin = ogTx.l1TxOrigin
      tx.queueOrigin = ogTx.queueOrigin
      tx.rawTransaction = ogTx.rawTransaction
      return tx
    })
    return parsed
  }

  // Modify the transaction formatter to include all the L2 fields for transactions.
  const ogTxResponseFormatter = formatter.transactionResponse.bind(formatter)
  formatter.transactionResponse = (tx: any) => {
    const parsed = ogTxResponseFormatter(tx)
    parsed.txType = tx.txType
    parsed.queueOrigin = tx.queueOrigin
    parsed.rawTransaction = tx.rawTransaction
    parsed.l1TxOrigin = tx.l1TxOrigin
    parsed.l1BlockNumber = tx.l1BlockNumber
      ? parseInt(tx.l1BlockNumber, 16)
      : tx.l1BlockNumbers
    return parsed
  }

  // Modify the receipt formatter to include all the L2 fields.
  const ogReceiptFormatter = formatter.receipt.bind(formatter)
  formatter.receipt = (receipt: any) => {
    const parsed = ogReceiptFormatter(receipt)
    parsed.l1GasPrice = toBigNumber(receipt.l1GasPrice)
    parsed.l1GasUsed = toBigNumber(receipt.l1GasUsed)
    parsed.l1Fee = toBigNumber(receipt.l1Fee)
    parsed.l1FeeScalar = parseFloat(receipt.l1FeeScalar)
    return parsed
  }

  // Connect extra functions.
  l2Provider.getL1GasPrice = async () => {
    return getL1GasPrice(l2Provider)
  }
  l2Provider.estimateL1Gas = async (tx: TransactionRequest) => {
    return estimateL1Gas(l2Provider, tx)
  }
  l2Provider.estimateL1GasCost = async (tx: TransactionRequest) => {
    return estimateL1GasCost(l2Provider, tx)
  }
  l2Provider.estimateL2GasCost = async (tx: TransactionRequest) => {
    return estimateL2GasCost(l2Provider, tx)
  }
  l2Provider.estimateTotalGasCost = async (tx: TransactionRequest) => {
    return estimateTotalGasCost(l2Provider, tx)
  }

  l2Provider._isL2Provider = true

  return l2Provider
}