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.