import Web3, { type BlockNumberOrTag, BlockTags, Contract, type DataFormat, DEFAULT_RETURN_FORMAT, FMT_BYTES, FMT_NUMBER, type Transaction, Web3PluginBase, } from 'web3' import { TransactionFactory, type TxData } from 'web3-eth-accounts' import { estimateGas, formatTransaction } from 'web3-eth' import { gasPriceOracleABI, gasPriceOracleAddress, } from '@eth-inura/contracts-ts' import { RLP } from '@ethereumjs/rlp' export class OptimismPlugin extends Web3PluginBase { public pluginNamespace = 'op' private _gasPriceOracleContract: | Contract | 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} - 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} - 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} - 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} - 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() .methods.getL1Fee(this._serializeTransaction(transaction)) .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} - 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( this._serializeTransaction(transaction) ) .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} - 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} - 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} - 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} - 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} - 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?: { blockNumber?: BlockNumberOrTag | undefined 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 * @param {DataFormat} [returnFormat=DEFAULT_RETURN_FORMAT] - The web3.js format object that specifies how to format number and bytes values * @returns {Promise} - 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, returnFormat?: ReturnFormat ) { const [l1Fee, l2Fee] = await Promise.all([ this.getL1Fee(transaction), this.getL2Fee(transaction), ]) return Web3.utils.format( { format: 'uint' }, l1Fee + l2Fee, returnFormat ?? DEFAULT_RETURN_FORMAT ) } /** * Used to get the web3.js contract instance for gas price oracle contract * @returns {Contract} - 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 */ private _serializeTransaction(transaction: Transaction) { 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 doesn't include a signature, // 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 { op: OptimismPlugin } }