Triggering OP Mainnet transactions from Ethereum

Triggering OP Stack transactions from Ethereum

OP Stack currently uses a single-Sequencer block production model. This means that there is only one Sequencer active on the network at any given time. Single-Sequencer models are simpler than their highly decentralized counterparts but they are also more vulnerable to potential downtime.

Sequencer downtime must not be able to prevent users from transacting on the network. As a result, OP Stack includes a mechanism for "forcing" transactions to be included in the blockchain. This mechanism involves triggering a transaction on OP Stack by sending a transaction on Ethereum. In this tutorial you'll learn how to trigger a transaction on OP Stack from Ethereum using Viem. You'll use the OP Sepolia testnet, but the same logic will apply to OP Stack.

Dependencies

Create a demo project

You're going to use the viem package for this tutorial. Since Viem is a Node.js (opens in a new tab) library, you'll need to create a Node.js project to use it.

Make a Project Folder

mkdir trigger-transaction
cd trigger-transaction

Initialize the Project

pnpm init

Install Viem

pnpm add viem

Want to create a new wallet for this tutorial? If you have cast (opens in a new tab) installed you can run cast wallet new in your terminal to create a new wallet and get the private key.

Get ETH on Sepolia and OP Sepolia

This tutorial explains how to bridge tokens from Sepolia to OP Sepolia. You will need to get some ETH on both of these testnets.

You can use this faucet (opens in a new tab) to get ETH on Sepolia. You can use the Superchain Faucet (opens in a new tab) to get ETH on OP Sepolia.

Add a private key to your environment

You need a private key in order to sign transactions. Set your private key as an environment variable with the export command. Make sure this private key corresponds to an address that has ETH on both Sepolia and OP Sepolia.

export TUTORIAL_PRIVATE_KEY=0x...

Start the Node REPL

You're going to use the Node REPL to interact with Viem. To start the Node REPL run the following command in your terminal:

node

This will bring up a Node REPL prompt that allows you to run javascript code.

Import dependencies

You need to import some dependencies into your Node REPL session.

Import Viem

  const { createPublicClient, createWalletClient, http, parseEther, formatEther } = require('viem');
  const { optimismSepolia, sepolia } = require('viem/chains');
  const { privateKeyToAccount } = require('viem/accounts');
  const { publicActionsL2, publicActionsL1, walletActionsL2, walletActionsL1, getL2TransactionHashes } = require ('viem/op-stack')

Set session variables

You'll need a few variables throughout this tutorial. Let's set those up now.

Load your private key

  const privateKey = process.env.TUTORIAL_PRIVATE_KEY;
  const account = privateKeyToAccount(privateKey);

Create the RPC providers and wallets

  const l1PublicClient = createPublicClient({ chain: sepolia, transport: http("https://rpc.ankr.com/eth_sepolia") }).extend(publicActionsL1())
  const l2PublicClient = createPublicClient({ chain: optimismSepolia, transport: http("https://sepolia.optimism.io") }).extend(publicActionsL2());
  const l1WalletClient = createWalletClient({ chain: sepolia, transport: http("https://rpc.ankr.com/eth_sepolia") }).extend(walletActionsL1());

Check your initial balance

You'll be sending a small amount of ETH as part of this tutorial. Quickly check your balance on OP Sepolia so that you know how much you had at the start of the tutorial.

  const initialBalance = await l2PublicClient.getBalance({ address });
  console.log(`Initial balance: ${formatEther(initialBalance)} ETH`);

Trigger the transaction

Now you'll use the OptimismPortal contract to trigger a transaction on OP Sepolia by sending a transaction on Sepolia.

Create the OptimismPortal object

  const optimismPortalAbi = [
      {
      inputs: [
          { internalType: 'uint256', name: '_gasLimit', type: 'uint256' },
          { internalType: 'bytes', name: '_data', type: 'bytes' },
      ],
      name: 'depositTransaction',
      outputs: [],
      stateMutability: 'payable',
      type: 'function',
      },
  ];

Estimate the required gas

When sending transactions via the OptimismPortal contract it's important to always include a gas buffer. This is because the OptimismPortal charges a variable amount of gas depending on the current demand for L2 transactions triggered via L1. If you do not include a gas buffer, your transactions may fail.

  const optimismPortalAddress = '0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383';
  const gasLimit = 100000n;
  const data = '0x';
  const value = parseEther('0.000069420');
  
  const gasEstimate = await l1PublicClient.estimateContractGas({
      address: optimismPortalAddress,
      abi: optimismPortalAbi,
      functionName: 'depositTransaction',
      args: [gasLimit, data],
      value,
      account: account.address,
  });

Send the transaction

Now you'll send the transaction. Note that you are including a buffer of 20% on top of the gas estimate.

  const { request } = await l1PublicClient.simulateContract({
      account,
      address: optimismPortalAddress,
      abi: optimismPortalAbi,
      functionName: 'depositTransaction',
      args: [gasLimit, data],
      value,
      gas: gasEstimate * 120n / 100n, // 20% buffer
    })
 
  const l1TxHash = await l1WalletClient.writeContract(request)
  console.log(`L1 transaction hash: ${l1TxHash}`)

Wait for the L1 transaction

First you'll need to wait for the L1 transaction to be mined.

  const l1TxHash = await l1WalletClient.writeContract(request)

Wait for the L2 transaction

Now you'll need to wait for the corresponding L2 transaction to be included in a block. This transaction is automatically created as a result of your L1 transaction. Here you'll determine the hash of the L2 transaction and then wait for that transaction to be included in the L2 blockchain.

  const [l2Hash] = getL2TransactionHashes(l1TxHash) 
  console.log(`Corresponding L2 transaction hash: ${l2Hash}`);
 
  const l2Receipt = await l2PublicClient.waitForTransactionReceipt({
      hash: l2Hash,
  }); 
  console.log('L2 transaction confirmed:', l2Receipt);

Check your updated balance

You should have a little less ETH on OP Sepolia now. Check your balance to confirm.

  const finalBalance = await l2Wallet.getBalance()
  console.log(`Final balance: ${formatEther(finalBalance)} ETH`);

Make sure that the difference is equal to the amount you were expecting to send.

  const difference = initialBalance - finalBalance
  console.log(`Difference in balance: ${formatEther(difference)} ETH`);

Next steps

You've successfully triggered a transaction on OP Sepolia by sending a transaction on Sepolia using Viem. Although this tutorial demonstrated the simple example of sending a basic ETH transfer from your L2 address via the OptimismPortal contract, you can use this same technique to trigger any transaction you want. You can trigger smart contracts, send ERC-20 tokens, and more.