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 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.
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.
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:
Copy
// Get Chain IDs using ethersasync 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 instancesasync 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.
Now you can create a function to bridge the ether between L1 and L2:
Copy
// Transfer an amount of ether from L1 to L2async 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.
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:
Copy
// Main functionasync function main() { await initialize() await bridgeEth()}// Run the main function and catch any errormain().then(() => process.exit(0)) .catch((error) => { console.error(error) process.exit(1) })
At this point, the entire code will look like this:
Copy
const ethers = require("ethers")const optimismSDK = require("@eth-optimism/sdk")require('dotenv').config()// Environment variablesconst SEPOLIA_CHAINSTACK = process.env.SEPOLIA_CHAINSTACK;const OPTIMISM_SEPOLIA_CHAINSTACK = process.env.OPTIMISM_SEPOLIA_CHAINSTACK;const PRIVATE_KEY = process.env.PRIVATE_KEY;// Provider instancesconst l1Provider = new ethers.providers.JsonRpcProvider(SEPOLIA_CHAINSTACK);const l2Provider = new ethers.providers.JsonRpcProvider(OPTIMISM_SEPOLIA_CHAINSTACK);// Init Signersasync 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 IDsasync 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 instancesasync 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 L2async 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 L2async 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 functionasync function main() { await initialize() await bridgeEth()}// Run the main functionmain().then(() => process.exit(0)) .catch((error) => { console.error(error) process.exit(1) })
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:
Copy
node index
The console will log all of the steps and it will look similar to the following:
Copy
Fetching current balances...Balance on L1: 9.7844 ETHBalance on L2: 0.9314 ETH----------------------------------Initiating ETH transfer from L1 to L2...Transaction hash for deposit from L1 to L2: 0x97455a64eb1c496f4ecc937ffcf2d9294228d9658504a16ab9dbfa638d32693aSee on Sepolia Etherscan: https://sepolia.etherscan.io/tx/0x97455a64eb1c496f4ecc937ffcf2d9294228d9658504a16ab9dbfa638d32693aWaiting for deposit transaction to be relayed...----------------------------------ETH transfer from L1 to L2 is complete.Updating current balances...----------------------------------Balance on L1: 9.6842 ETHBalance 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.