plugin.ts 12.6 KB
Newer Older
Wyatt Barnes's avatar
Wyatt Barnes committed
1
import Web3, {
2
  type BlockNumberOrTag,
Wyatt Barnes's avatar
Wyatt Barnes committed
3 4
  BlockTags,
  Contract,
5
  type DataFormat,
Wyatt Barnes's avatar
Wyatt Barnes committed
6 7 8
  DEFAULT_RETURN_FORMAT,
  FMT_BYTES,
  FMT_NUMBER,
9
  type Transaction,
Wyatt Barnes's avatar
Wyatt Barnes committed
10 11
  Web3PluginBase,
} from 'web3'
12
import { TransactionFactory, type TxData } from 'web3-eth-accounts'
Wyatt Barnes's avatar
Wyatt Barnes committed
13 14 15 16 17 18 19
import { estimateGas, formatTransaction } from 'web3-eth'
import {
  gasPriceOracleABI,
  gasPriceOracleAddress,
} from '@eth-optimism/contracts-ts'
import { RLP } from '@ethereumjs/rlp'

20
export class OptimismPlugin extends Web3PluginBase {
Wyatt Barnes's avatar
Wyatt Barnes committed
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
  public pluginNamespace = 'op'

  private _gasPriceOracleContract:
    | Contract<typeof gasPriceOracleABI>
    | undefined

  /**
   * Retrieves the current L2 base fee
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<bigint>} - The L2 base fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const baseFeeValue: bigint = await web3.op.getBaseFee();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const baseFeeValue: number = await web3.op.getBaseFee(numberFormat);
   */
  public async getBaseFee<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.baseFee().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the decimals used in the scalar
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The number of decimals as a BigInt by default, but {returnFormat} determines type
   * @example
   * const decimalsValue: bigint = await web3.op.getDecimals();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const decimalsValue: number = await web3.op.getDecimals(numberFormat);
   */
  public async getDecimals<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.decimals().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the current L2 gas price (base fee)
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The current L2 gas price as a BigInt by default, but {returnFormat} determines type
   * @example
   * const gasPriceValue: bigint = await web3.op.getGasPrice();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const gasPriceValue: number = await web3.op.getGasPrice(numberFormat);
   */
  public async getGasPrice<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.gasPrice().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * 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
   * @param transaction - An unsigned web3.js {Transaction} object
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const l1FeeValue: bigint = await getL1Fee(transaction);
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const l1FeeValue: number = await getL1Fee(transaction, numberFormat);
   */
  public async getL1Fee<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(transaction: Transaction, returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance()
105
        .methods.getL1Fee(this._serializeTransaction(transaction))
Wyatt Barnes's avatar
Wyatt Barnes committed
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
        .call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Computes the amount of L1 gas used for {transaction}. Adds the overhead which
   * represents the per-transaction gas overhead of posting the {transaction} and state
   * roots to L1. Adds 68 bytes of padding to account for the fact that the input does
   * not have a signature.
   * @param transaction - An unsigned web3.js {Transaction} object
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The amount gas as a BigInt by default, but {returnFormat} determines type
   * @example
   * const gasUsedValue: bigint = await getL1GasUsed(transaction);
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const gasUsedValue: number = await getL1GasUsed(transaction, numberFormat);
   */
  public async getL1GasUsed<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(transaction: Transaction, returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance()
        .methods.getL1GasUsed(
132
          this._serializeTransaction(transaction)
Wyatt Barnes's avatar
Wyatt Barnes committed
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 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
        )
        .call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the latest known L1 base fee
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The L1 base fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const baseFeeValue: bigint = await web3.op.getL1BaseFee();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const baseFeeValue: number = await web3.op.getL1BaseFee(numberFormat);
   */
  public async getL1BaseFee<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.l1BaseFee().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the current fee overhead
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The current overhead fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const overheadValue: bigint = await web3.op.getOverhead();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const overheadValue: number = await web3.op.getOverhead(numberFormat);
   */
  public async getOverhead<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.overhead().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the current fee scalar
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The current scalar fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const scalarValue: bigint = await web3.op.getScalar();
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const scalarValue: number = await web3.op.getScalar(numberFormat);
   */
  public async getScalar<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(returnFormat?: ReturnFormat) {
    return Web3.utils.format(
      { format: 'uint' },
      await this._getPriceOracleContractInstance().methods.scalar().call(),
      returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Retrieves the full semver version of GasPriceOracle
   * @returns {Promise<string>} - The semver version
   * @example
   * const version = await web3.op.getVersion();
   */
  public async getVersion() {
    return this._getPriceOracleContractInstance().methods.version().call()
  }

  /**
   * Retrieves the amount of L2 gas estimated to execute {transaction}
   * @param transaction - An unsigned web3.js {Transaction} object
   * @param {{ blockNumber: BlockNumberOrTag, returnFormat: DataFormat }} [options={blockNumber: BlockTags.LATEST, returnFormat: DEFAULT_RETURN_FORMAT}] -
   * An options object specifying what block to use for gas estimates and the web3.js format object that specifies how to format number and bytes values
   * @returns {Promise<Numbers>} - The gas estimate as a BigInt by default, but {returnFormat} determines type
   * @example
   * const l2Fee: bigint = await getL2Fee(transaction);
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const l2Fee: number = await getL2Fee(transaction, numberFormat);
   */
  public async getL2Fee<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(
    transaction: Transaction,
    options?: {
226
      blockNumber?: BlockNumberOrTag | undefined
Wyatt Barnes's avatar
Wyatt Barnes committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
      returnFormat?: ReturnFormat
    }
  ) {
    const [gasCost, gasPrice] = await Promise.all([
      estimateGas(
        this,
        transaction,
        options?.blockNumber ?? BlockTags.LATEST,
        DEFAULT_RETURN_FORMAT
      ),
      this.getGasPrice(),
    ])

    return Web3.utils.format(
      { format: 'uint' },
      gasCost * gasPrice,
      options?.returnFormat ?? DEFAULT_RETURN_FORMAT
    )
  }

  /**
   * Computes the total (L1 + L2) fee estimate to execute {transaction}
   * @param transaction - An unsigned web3.js {Transaction} object
250
   * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values
Wyatt Barnes's avatar
Wyatt Barnes committed
251 252 253 254 255 256 257 258 259 260 261
   * @returns {Promise<Numbers>} - The estimated total fee as a BigInt by default, but {returnFormat} determines type
   * @example
   * const estimatedFees: bigint = await estimateFees(transaction);
   * @example
   * const numberFormat = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }
   * const estimatedFees: number = await estimateFees(transaction, numberFormat);
   */
  public async estimateFees<
    ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT
  >(
    transaction: Transaction,
262
    returnFormat?: ReturnFormat
Wyatt Barnes's avatar
Wyatt Barnes committed
263 264 265
  ) {
    const [l1Fee, l2Fee] = await Promise.all([
      this.getL1Fee(transaction),
266
      this.getL2Fee(transaction),
Wyatt Barnes's avatar
Wyatt Barnes committed
267 268 269 270 271
    ])

    return Web3.utils.format(
      { format: 'uint' },
      l1Fee + l2Fee,
272
      returnFormat ?? DEFAULT_RETURN_FORMAT
Wyatt Barnes's avatar
Wyatt Barnes committed
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    )
  }

  /**
   * Used to get the web3.js contract instance for gas price oracle contract
   * @returns {Contract<typeof gasPriceOracleABI>} - A web.js contract instance with an RPC provider inherited from root {web3} instance
   */
  private _getPriceOracleContractInstance() {
    if (this._gasPriceOracleContract === undefined) {
      this._gasPriceOracleContract = new Contract(
        gasPriceOracleABI,
        gasPriceOracleAddress[420]
      )
      // This plugin's Web3Context is overridden with main Web3 instance's context
      // when the plugin is registered. This overwrites the Contract instance's context
      this._gasPriceOracleContract.link(this)
    }

    return this._gasPriceOracleContract
  }

  /**
   * Returns the RLP encoded hex string for {transaction}
   * @param transaction - A web3.js {Transaction} object
   * @returns {string} - The RLP encoded hex string
   */
299
  private _serializeTransaction(transaction: Transaction) {
Wyatt Barnes's avatar
Wyatt Barnes committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    const ethereumjsTransaction = TransactionFactory.fromTxData(
      formatTransaction(transaction, {
        number: FMT_NUMBER.HEX,
        bytes: FMT_BYTES.HEX,
      }) as TxData
    )

    return Web3.utils.bytesToHex(
      Web3.utils.uint8ArrayConcat(
        Web3.utils.hexToBytes(
          ethereumjsTransaction.type.toString(16).padStart(2, '0')
        ),
        // If <transaction> doesn't include a signature,
        // <ethereumjsTransaction.raw()> will autofill v, r, and s
        // with empty uint8Array. Because L1 fee calculation
        // is dependent on the number of bytes, we are removing
        // the zero values bytes
        RLP.encode(ethereumjsTransaction.raw().slice(0, -3))
      )
    )
  }
}

// Module Augmentation to add op namespace to root {web3} instance
declare module 'web3' {
  interface Web3Context {
326
    op: OptimismPlugin
Wyatt Barnes's avatar
Wyatt Barnes committed
327 328
  }
}