Skip to content

Latest commit

 

History

History
 
 

sdk-estimate-gas

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Estimate the costs of an OP Mainnet (L2) transaction

Discord Twitter Follow

This tutorial teaches you how to use the Optimism SDK to estimate the gas costs of L2 transactions. This calculation is complicated by the fact that the major cost is the cost of writing the transaction on L1, it doesn't work to just multiply the gas used by the transaction by the gas price, the same way you would on L1. You can read the details of the calculation here.

Prerequisites

The node script makes these assumptions:

  • You have Node.js running on your computer, as well as yarn.
  • There is network connectivity to a provider on the Optimistic Goerli L2 network, and to the npm package registry.

Running the script

  1. Use yarn to download the packages you need

    yarn
  2. Copy .env.example to .env and modify the parameters:

    • MNEMONIC is the mnemonic to an account that has enough ETH to pay for the transaction.

    • ALCHEMY_API_KEY is the API key for an OP Mainnet or OP Goerli app on Alchemy, our preferred provider.

    • OPTIMISM_GOERLI_URL is the URL for OP Goerli, if you use a different node provider.

    • OPTIMISM_MAINNET_URL is the URL for OP Mainnet, if you use a different node provider.

  3. Use Node to run the script

    node gas.js --network mainnet

    The command line options are:

    • --network: The network to estimate gas on:

      • mainnet: OP Mainnet
      • goerli: OP Goerli
    • --verify: Run the transaction to verify the estimate

Results

Here is an example of results from OP Mainnet:

ori@Oris-MBP sdk-estimate-gas % ./gas.js --network mainnet --verify
ori@Oris-MacBook-Pro sdk-estimate-gas % ./gas.js --network mainnet --verify
About to get estimates
About to create the transaction
Transaction created and submitted
Transaction processed
Estimates:
   Total gas cost:       58819800030256 wei
      L1 gas cost:       58787232030256 wei
      L2 gas cost:          32568000000 wei

Real values:
   Total gas cost:       58819786030272 wei
      L1 gas cost:       58787232030256 wei
      L2 gas cost:          32554000016 wei

L1 Gas:
      Estimate:       4276
          Real:       4276
    Difference:          0

L2 Gas:
      Estimate:      32568
          Real:      32554
    Difference:        -14

The L1 gas cost is over a thousand times the L2 gas cost. This is typical in Optimistic transactions, because of the cost ratio between L1 gas and L2 gas.

How does it work?

In this section we go over the relevant parts of the script.

Setup

#! /usr/local/bin/node

// Estimate the costs of an Optimistic (L2) transaction

const ethers = require("ethers")
const optimismSDK = require("@eth-optimism/sdk")
const fs = require("fs")
require('dotenv').config()
const yargs = require("yargs")
const { boolean } = require("yargs")

The packages needed for the script.

const argv = yargs
  .option('network', {
    // mainnet - OP Mainnet, the production network
    // goerli - OP Goerli, the main test network
    choices: ["mainnet", "goerli"],
    description: 'OP network to use'
  }).
  option('verify', {
    type: boolean,
    description: 'Run the transaction, compare to the estimate'
  })
  .help()
  .alias('help', 'h').argv;

Use the yargs package to read the command line parameters.

const greeterJSON = JSON.parse(fs.readFileSync("Greeter.json")) 

Read the JSON file to know how to use the Greeter contract.

// These are the addresses of the Greeter.sol contract on the various Optimism networks:
// mainnet - OP Mainnet, the production network
// goerli - OP Goerli, the main test network
const greeterAddrs = {
  "mainnet":  "0xcf210488dad6da5fe54d260c45253afc3a9e708c",
  "goerli": "0x106941459a8768f5a92b770e280555faf817576f"
}

Addresses for the Greeter contracts:

// Utilities
const displayWei = x => x.toString().padStart(20, " ")                        
const displayGas = x => x.toString().padStart(10, " ")

Display a value (either wei or gas). To properly align these values for display, we first turn them into strings and then add spaces to the start until the total value is the right length (20 or 10 characters).

const sleep = ms => new Promise(resp => setTimeout(resp, ms));

Return a Promise that gets resolved after ms milliseconds.

getSigner

const getSigner = async () => {
  let endpointUrl;

  if (argv.network == 'goerli')
    endpointUrl = 
      process.env.ALCHEMY_API_KEY ? 
        `https://opt-goerli.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}` :
        process.env.OPTIMISM_GOERLI_URL
  if (argv.network == 'mainnet')
    endpointUrl = 
      process.env.ALCHEMY_API_KEY ? 
        `https://opt-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}` :
        process.env.OPTIMISM_MAINNET_URL

    const l2RpcProvider = optimismSDK.asL2Provider(
      new ethers.providers.JsonRpcProvider(endpointUrl)
    )

The function optimismSDK.asL2Provider takes a regular Ethers.js Provider and adds a few L2 specific functions, which are explained below. Because it only adds functions, an L2Provider can be used anywhere you use an Ethers Provider.

    const wallet = ethers.Wallet.fromMnemonic(process.env.MNEMONIC).
      connect(l2RpcProvider)

    return wallet
}   // getSigner

getEstimates

// Get estimates from the SDK
const getEstimates = async (provider, tx) => {
  return {
    totalCost: await provider.estimateTotalGasCost(tx),

Estimate the total cost (L1+L2) of running the transaction.

⚠️ This function calls eth_estimateGas, which runs the transaction in the node (without changing the blockchain state). This means that the account in l2Provider has to have enough ETH to pay for the gas cost of the transaction.

    l1Cost: await provider.estimateL1GasCost(tx),
    l2Cost: await provider.estimateL2GasCost(tx),

Estimate the two components of the cost: L1 and L2.

    l1Gas: await provider.estimateL1Gas(tx)
  }
}    // getEstimates

Get the amount of gas we expect to use to store the transaction on L1.

displayResults

const displayResults = (estimated, real) => {
  console.log(`Estimates:`)
  console.log(`   Total gas cost: ${displayWei(estimated.totalCost)} wei`)
  console.log(`      L1 gas cost: ${displayWei(estimated.l1Cost)} wei`)
  console.log(`      L2 gas cost: ${displayWei(estimated.l2Cost)} wei`)

Show the gas cost estimates.

  if (argv.verify) {
    console.log(`\nReal values:`)    
    console.log(`   Total gas cost: ${displayWei(real.totalCost)} wei`)
    console.log(`      L1 gas cost: ${displayWei(real.l1Cost)} wei`)
    console.log(`      L2 gas cost: ${displayWei(real.l2Cost)} wei`)

If we are verifying the estimates, show the real values.

    console.log(`\nL1 Gas:`)
    console.log(`      Estimate: ${displayGas(estimated.l1Gas)}`)
    console.log(`          Real: ${displayGas(real.l1Gas)}`)  
    console.log(`    Difference: ${displayGas(real.l1Gas-estimated.l1Gas)}`)

Compare the L1 gas estimated with the L1 gas actually required.

    console.log(`\nL2 Gas:`)
    console.log(`      Estimate: ${displayGas(estimated.l2Gas)}`)
    console.log(`          Real: ${displayGas(real.l2Gas)}`)  
    console.log(`    Difference: ${displayGas(real.l2Gas-estimated.l2Gas)}`)

Compare the L2 gas estimates with the L2 gas actually required.

  } else {   // if argv.verify
    console.log(`      L1 gas: ${displayGas(estimated.l1Gas)}`)
    console.log(`      L2 gas: ${displayGas(estimated.l2Gas)}`)
  }   // if argv.verify

}   // displayResults

If we aren't verifying the estimate, just display the estimated values.

main

const main = async () => {    
    
    const signer = await getSigner()

    if(!greeterAddrs[argv.network]) {
      console.log(`I don't know the Greeter address on chain: ${argv.network}`)
      process.exit(-1)  
    }

    const Greeter = new ethers.ContractFactory(greeterJSON.abi, greeterJSON.bytecode, signer)
    const greeter = Greeter.attach(greeterAddrs[argv.network])

    const greeting = "Hello!"

    let real = {}

To create a valid estimate, we need these transaction fields:

  • data
  • to
  • gasPrice
  • type
  • nonce
  • gasLimit

We need the exact values, because a zero costs only 4 gas and any other byte costs 16 bytes. For example, it is cheaper to encode gasLimit if it is 0x100000 rather than 0x10101.

    const fakeTxReq = await greeter.populateTransaction.setGreeting(greeting)

Ether's populateTransaction function gives us three fields:

  • data
  • from
  • to
    const fakeTx = await signer.populateTransaction(fakeTxReq)

The contract cannot provide us with the nonce, chainId, gasPrice, or gasLimit. To get those fields we use signer.populateTransaction.

    console.log("About to get estimates")
    let estimated = await getEstimates(signer.provider, fakeTx)

Call getEstimates to get the L2Provider estimates.

    estimated.l2Gas = await greeter.estimateGas.setGreeting(greeting)

There is no need for a special function to estimate the amount of L2 gas, the normal estimateGas.<function> can do the same job it usually does.

    if (argv.verify) {

// If we want to run the real transaction to verify the estimate

      // If the transaction fails, error out with additional information
      let realTx, realTxResp
      const weiB4 = await signer.getBalance()

Get the balance prior to the transaction, so we'll be able to see how much it really cost.

      try {
        console.log("About to create the transaction")
        realTx = await greeter.setGreeting(greeting)
        console.log("Transaction created, submitting it")
        realTxResp = await realTx.wait()
        console.log("Transaction processed")

Create the transaction and then wait for it to be processed. This is the standard way to submit a transaction in Ethers.

      } catch (err) {        
        console.log(`Error: ${err}`)
        console.log(`Coming from address: ${await signer.getAddress()} on Optimistic ${network}`)
        console.log(`            balance: ${displayWei(await signer.getBalance())} wei`)
        process.exit(-1)
      }

If the transaction failed, it could be because the account lacks the ETH to pay for gas. The error message shows that information so the user knows about it.

      // If the balance hasn't been updated yet, wait 0.1 sec
      real.totalCost = 0
      while (real.totalCost === 0) {
          const weiAfter = await signer.getBalance()
          real.totalCost= weiB4-weiAfter
          await sleep(100)
      }

It takes a bit of time before the change in the account's balance is processed. This loop lets us wait until it is processed so we'll be able to know the full cost.

Note that this is not the only way to wait until a transaction happens. You can also use crossDomainMessenger.waitForMessageStatus.

      // Get the real information (cost, etc.) from the transaction response
      real.l1Gas = realTxResp.l1GasUsed
      real.l1Cost = realTxResp.l1Fee 

These fields are specific to OP Mainnet and OP Goerli transaction responses.

      real.l2Gas = realTxResp.gasUsed

The gas used on L2 is the gas used for processing. This field is standard in Ethers.

      real.l2Cost = real.totalCost - real.l1Cost
    }  // if argv.verified

This is one way to get the L2 cost of the transaction. Another would be to multiply gasUsed by gasPrice.

    displayResults(estimated, real)    
}  // main


main().then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Conclusion

Using the Optimism SDK you can show users how much a transaction would cost before they submit it. This is a useful feature in decentralized apps, because it lets people decide if the transaction is worth doing or not.