Optimism: Bridge ether from Ethereum L1 to Optimism L2 using the Optimism JavaScript SDK

This tutorial will show you how to use the Optimism JavaScript SDK to bridge ether from L1 to L2.

Optimism

Optimism is a next-generation solution that enhances the Ethereum blockchain by providing a supplementary layer 2 network. Optimism streamlines the transaction process on Ethereum, resulting in significantly lower fees and fast execution. The beauty of Optimism lies in its seamless integration with Ethereum—each transaction takes place on the Optimism network, yet its validity is confirmed via the Ethereum blockchain.

Optimism's inner working

Optimism leverages the breakthrough technology of optimistic rollups, a sophisticated compression technique developed by the team at the Optimism Foundation. Rollups are a new way to scale the Ethereum blockchain and come in optimistic rollups and zero-knowledge rollups (ZK rollups).

Optimistic rollups streamline the transaction process by taking the bulk of data off-chain, resulting in faster processing times. Despite this off-chain approach, a small amount of data is still recorded on the Ethereum network for security purposes.

What sets optimistic rollups apart from other scaling solutions is that they do not require cryptographic proofs to validate off-chain transactions. Instead, they rely on a system of fraud proofs and utilize the Optimistic Virtual Machine (OVM)—a sandboxed environment—to ensure secure and deterministic smart contract execution.

The OVM acts as the interface between Layers 1 and 2, much like the Ethereum Virtual Machine (EVM). However, the OVM only executes computation, while the EVM handles all execution. The OVM and EVM work together through the Execution Manager to execute transactions in a virtualized environment.

Learn more about the Optimism network in Optimism blockchain – Ethereum Layer 2 scaling solution on the Chainstack blog.

Prerequisites

Dependencies

  • @eth-optimism/sdk: ^1.10.1
  • dotenv: ^16.0.3
  • Ethers: ^5.7.2

Overview

To get from zero to a functioning bridge between Ethereum L1 and Optimism L2, do the following:

  1. With Chainstack, create a public chain project.
  2. With Chainstack, join the Ethereum Sepolia testnet and Optimism Sepolia testnet.
  3. With Chainstack, access your nodes' credentials.
  4. Create an npm project.
  5. Install the required dependencies.
  6. Create a .env file to store the secrets.
  7. With the Ethers library, create provider and wallet instances.
  8. With Optimism JavaScript SDK, bridge ether between L1 and L2.
  9. Run the script.

Step-by-step

Create a public chain project

See Create a project.

Join the Ethereum Sepolia testnet and Optimism Goerli testnet

See Join a public network.

Get endpoints for your Ethereum Sepolia testnet and Optimism Sepolia testnet

See View node access and credentials.

Create an npm project

In your project directory, open the terminal and run npm init. Answer the questions in the terminal to create a sample package.json file for your project.

Install the required dependencies

To install the libraries and tools required for this bridge project, run:

npm i @eth-optimism/sdk ethers dotenv

Create a .env file to store the secrets

Create a .env file in the root directory of your project and paste and fill in the following:

PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY"
SEPOLIA_CHAINSTACK="YOUR_CHAINSTACK_SEPOLIA_ENDPOINT"
OPTIMISM_GOERLI_CHAINSTACK="YOUR_CHAINSTACK_OPTIMISM_GOERLI_ENDPOINT"

Use the Ethers library to create provider and wallet instances

Create an index.js file in your project's root directory.

The first step is to import the required packages and create the global variables. To do that, at the top of the file, paste the following:

const ethers = require("ethers")
const optimismSDK = require("@eth-optimism/sdk")
require('dotenv').config()

// Environment variables
const SEPOLIA_CHAINSTACK = process.env.SEPOLIA_CHAINSTACK;
const OPTIMISM_SEPOLIA_CHAINSTACK = process.env.OPTIMISM_SEPOLIA_CHAINSTACK;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

// Provider instances
const l1Provider = new ethers.providers.JsonRpcProvider(GOERLI_CHAINSTACK);
const l2Provider = new ethers.providers.JsonRpcProvider(OPTIMISM_GOERLI_CHAINSTACK);

// Init Signers
async function getSigners() {
  const privateKey = PRIVATE_KEY;
  const l1Wallet = new ethers.Wallet(privateKey, l1Provider);
  const l2Wallet = new ethers.Wallet(privateKey, l2Provider);

  return [l1Wallet, l2Wallet];
}

This part of the code does the following:

  • Imports libraries and tools.
  • Declares the required constants using the environment variables.
  • Creates provider instances for L1 (Ethereum Sepolia testnet) and L2 (Optimism Sepolia testnet).
  • Creates wallet instances. The getSigners() function uses the private key to create wallet instances with the Ethers library. These instances will be used to query balances and sign transactions.

Use the Optimism JavaScript SDK to bridge ether between L1 and L2

Create a function to retrieve the networks' chain IDs and a CrossChainMessenger instance using the Optimism JavaScript SDK:

// Get Chain IDs using ethers
async function chainIds() {
  const l1Network= await l1Provider.getNetwork();
  const l2Network = await l2Provider.getNetwork();

  const l1ChainId = l1Network.chainId
  const l2ChainId = l2Network.chainId

  return [l1ChainId, l2ChainId]

}

// Init crossChainMessenger using the Chain IDs and wallet instances
async function initialize() {

  const [l1Signer, l2Signer] = await getSigners()
  const [l1ChainId, l2ChainId] = await chainIds()

  crossChainMessenger = new optimismSDK.CrossChainMessenger({
      l1ChainId: l1ChainId,    
      l2ChainId: l2ChainId,   
      l1SignerOrProvider: l1Signer,
      l2SignerOrProvider: l2Signer
  })
}  

The chainIds() function uses the Ethers library to query the Chainstack endpoints and retrieve the chain IDs that will then be used to create the crossChainMessenger instance.

The initialize() function uses the getSigners() and chainIds() functions to retrieve the parameters required to create the crossChainMessenger using the Optimism JavaScript SDK. The crossChainMessenger instance allows us to interact with the L1 and L2 networks.

Function to retrieve the wallets balances

Add the following function to retrieve the wallet balances and display them in the console:

// Display balances from L1 and L2 using the Optimism SDK crossChainMessenger provider instance
async function showBalances() {
  const l1Balance = (await crossChainMessenger.l1Signer.getBalance()).toString();
  const l2Balance = (await crossChainMessenger.l2Signer.getBalance()).toString();

  console.log(`Balance on L1: ${ethers.utils.formatEther(l1Balance).slice(0,-14)} ETH`);
  console.log(`Balance on L2: ${ethers.utils.formatEther(l2Balance).slice(0,-14)} ETH`);
  console.log("----------------------------------");
}  

📘

Note

The console.log statements with the - are there only to make the response in the console more legible.

Function to transfer ether between L1 and L2

Now you can create a function to bridge the ether between L1 and L2:

// Transfer an amount of ether from L1 to L2
async function bridgeEth() {

  const wei = BigInt(100000000000000000); // 0.1 ETH in Wei
  console.log("Fetching current balances...");

  await showBalances();
  console.log("Initiating ETH transfer from L1 to L2...");

  const depositResponse = await crossChainMessenger.depositETH(wei);
  console.log(`Transaction hash for deposit from L1 to L2: ${depositResponse.hash}`);
  console.log(`See on Sepolia Etherscan: https://sepolia.etherscan.io/tx/${depositResponse.hash}`);

  await depositResponse.wait();
  console.log("Waiting for deposit transaction to be relayed...");
  console.log("----------------------------------");

  await crossChainMessenger.waitForMessageStatus(
    depositResponse.hash,
    optimismSDK.MessageStatus.RELAYED
  );
  console.log("ETH transfer from L1 to L2 is complete.");
  console.log("Updating current balances...");
  console.log("----------------------------------");

  await showBalances();
}

Note that the const wei holds the amount that will be transferred, expressed in the Wei unit.

The default for this script is set to 0.1 ether, equivalent to 100,000,000,000,000,000 Wei. You can use a Wei converter to include other amounts, or you can use the following:

  • 1 ether = 1,000,000,000,000,000,000 Wei
  • 0.1 ether = 100,000,000,000,000,000 Wei
  • 0.01 ether = 10,000,000,000,000,000 Wei
  • 0.001 ether = 1,000,000,000,000,000 Wei

The bridgeEth() function transfers the amount of ether specified in the wei constant from Ethereum (L1) to Optimism (L2). It uses the depositEth method of the crossChainMessenger instance.

It then waits for the MessageStatus to become RELAYED. The RELAYED status indicates that a message has been successfully transmitted from one network to another and is under processing on the recipient network.

📘

Message statuses

Find the different message statuses in the Optimism JavaScript SDK docs.

After that, the bridgeEth() function gives some updates and retrieves the balances again.

At the bottom of the file, add the main function and call it:

// Main function
async function main() {
    await initialize()
    await bridgeEth()
}

// Run the main function and catch any error
main().then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })

At this point, the entire code will look like this:

const ethers = require("ethers")
const optimismSDK = require("@eth-optimism/sdk")
require('dotenv').config()

// Environment variables
const SEPOLIA_CHAINSTACK = process.env.SEPOLIA_CHAINSTACK;
const OPTIMISM_SEPOLIA_CHAINSTACK = process.env.OPTIMISM_SEPOLIA_CHAINSTACK;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

// Provider instances
const l1Provider = new ethers.providers.JsonRpcProvider(SEPOLIA_CHAINSTACK);
const l2Provider = new ethers.providers.JsonRpcProvider(OPTIMISM_SEPOLIA_CHAINSTACK);

// Init Signers
async function getSigners() {
    const privateKey = PRIVATE_KEY;
    const l1Wallet = new ethers.Wallet(privateKey, l1Provider);
    const l2Wallet = new ethers.Wallet(privateKey, l2Provider);

    return [l1Wallet, l2Wallet];
}

// Get Chain IDs
async function chainIds() {
    const l1Network = await l1Provider.getNetwork();
    const l2Network = await l2Provider.getNetwork();

    const l1ChainId = l1Network.chainId
    const l2ChainId = l2Network.chainId

    return [l1ChainId, l2ChainId]

}

// Init crossChainMessenger using the Chain IDs and wallet instances
async function initialize() {

    const [l1Signer, l2Signer] = await getSigners()
    const [l1ChainId, l2ChainId] = await chainIds()

    crossChainMessenger = new optimismSDK.CrossChainMessenger({
        l1ChainId: l1ChainId,
        l2ChainId: l2ChainId,
        l1SignerOrProvider: l1Signer,
        l2SignerOrProvider: l2Signer
    })
}

// Display balances from L1 and L2
async function showBalances() {
    const l1Balance = (await crossChainMessenger.l1Signer.getBalance()).toString();
    const l2Balance = (await crossChainMessenger.l2Signer.getBalance()).toString();

    console.log(`Balance on L1: ${ethers.utils.formatEther(l1Balance).slice(0,-14)} ETH`);
    console.log(`Balance on L2: ${ethers.utils.formatEther(l2Balance).slice(0,-14)} ETH`);
    console.log("----------------------------------");
}

// Transfer an amount of ether from L1 to L2
async function bridgeEth() {

    const wei = BigInt(100000000000000000); // 0.1 ETH in Wei
    console.log("Fetching current balances...");

    await showBalances();
    console.log("Initiating ETH transfer from L1 to L2...");

    const depositResponse = await crossChainMessenger.depositETH(wei);
    console.log(`Transaction hash for deposit from L1 to L2: ${depositResponse.hash}`);
    console.log(`See on Goerli Etherscan: https://goerli.etherscan.io/tx/${depositResponse.hash}`);

    await depositResponse.wait();
    console.log("Waiting for deposit transaction to be relayed...");
    console.log("----------------------------------");

    await crossChainMessenger.waitForMessageStatus(
        depositResponse.hash,
        optimismSDK.MessageStatus.RELAYED
    );
    console.log("ETH transfer from L1 to L2 is complete.");
    console.log("Updating current balances...");
    console.log("----------------------------------");

    await showBalances();
}

// Main function
async function main() {
    await initialize()
    await bridgeEth()
}

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

Run the script

Now it's time to run the script and bridge some ether from L1 to L2. To do this, you will need some Sepolia ether in your wallet. To get testnet ether, you can use the following faucet:

Once you have received some Sepolia ether, pick the amount you want to send and update the wei constant.

To start the script, run the following command:

node index

The console will log all of the steps and it will look similar to the following:

Fetching current balances...
Balance on L1: 9.7844 ETH
Balance on L2: 0.9314 ETH
----------------------------------      
Initiating ETH transfer from L1 to L2...
Transaction hash for deposit from L1 to L2: 0x97455a64eb1c496f4ecc937ffcf2d9294228d9658504a16ab9dbfa638d32693a
See on Sepolia Etherscan: https://sepolia.etherscan.io/tx/0x97455a64eb1c496f4ecc937ffcf2d9294228d9658504a16ab9dbfa638d32693a
Waiting for deposit transaction to be relayed...
----------------------------------
ETH transfer from L1 to L2 is complete.
Updating current balances...
----------------------------------
Balance on L1: 9.6842 ETH
Balance on L2: 1.0314 ETH
----------------------------------

As you can see, it prints the transaction hash and the link to check the transaction details using Sepolia Etherscan.

See the details of one of the already completed transactions on Etherscan.

Conclusion

This tutorial guided you through creating a basic L1 => L2 bridge using the Optimism JavaScript SDK.

About the author

Davide Zambiasi

🥑 Developer Advocate @ Chainstack
🛠️ BUIDLs on EVM, The Graph protocol, and Starknet
💻 Helping people understand Web3 and blockchain development
Davide Zambiasi | GitHub Davide Zambiasi | Twitter Davide Zambiasi | LinkedIN