L1->L2 Main

Step-by-step

Sending an L1->L2 transaction requires following these steps:

  1. Initialise the providers and the wallet that will send the transaction:
      const [wallet] = await ethers.getWallets();
    
      // Initialize the L2 provider.
      const l2Provider = ethers.providerL2;
    
      // Initialize the L1 provider.
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const networkInfo = network as any;
      const l1RPCEndpoint =
        networkInfo.config.ethNetwork === 'http://localhost:8545'
          ? networkInfo.config.ethNetwork
          : '<YOUR_L1_RPC_ENDPOINT>';
      const l1Provider = new ethers.Provider(l1RPCEndpoint);
    
  2. Retrieve the current gas price on L1.
      // retrieve L1 gas price
      const l1GasPrice = await l1Provider.getGasPrice();
      console.log(`L1 gasPrice ${ethers.formatEther(l1GasPrice)} ETH`);
    
  3. Populate the transaction that you want to execute on L2:
      const contract = new ethers.Contract(L2_CONTRACT_ADDRESS, GREETER_ABI_JSON.abi, wallet);
      const message = `Message sent from L1 at ${new Date().toUTCString()}`;
      // populate tx object
      const tx = await contract.setGreeting.populateTransaction(message);
    
  4. Retrieve the gas limit for sending the populated transaction to L2 using estimateGasL1(). This function calls the zks_estimateGasL1ToL2 RPC method under the hood:
      // Estimate gas limit for L1-L2 tx
      const l2GasLimit = await l2Provider.estimateL1ToL2Execute({
        contractAddress: L2_CONTRACT_ADDRESS,
        calldata: tx.data,
        caller: wallet.address,
      });
      console.log(`L2 gasLimit ${l2GasLimit.toString()}`);
    
  5. Calculate the total transaction fee to cover the cost of sending the transaction on L1 and executing the transaction on L2 using getBaseCost from the Wallet class. This method:
    • Retrieves the ZKsync BridgeHub contract address by calling zks_getBridgehubContract RPC method.
    • Calls the l2TransactionBaseCost function on the ZKsync BridgeHub system contract to retrieve the fee.
      const baseCost = await wallet.getBaseCost({
        // L2 computation
        gasLimit: l2GasLimit,
        // L1 gas price
        gasPrice: l1GasPrice,
      });
    
      console.log(`Executing this transaction will cost ${ethers.formatEther(baseCost)} ETH`);
    
  6. Encode the transaction calldata:
      const iface = new ethers.Interface(GREETER_ABI_JSON.abi);
      const calldata = iface.encodeFunctionData('setGreeting', [message]);
    
  7. Finally, send the transaction from your wallet using the requestExecute method. This helper method:
    • Retrieves the Zksync BridgeHub contract address calling zks_getBridgehubContract.
    • Populates the L1-L2 transaction.
    • Sends the transaction to the requestL2TransactionDirect function of the ZKsync BridgeHub system contract on L1.
      const txReceipt = await wallet.requestExecute({
        // destination contract in L2
        contractAddress: L2_CONTRACT_ADDRESS,
        calldata,
        l2GasLimit: l2GasLimit,
        refundRecipient: wallet.address,
        overrides: {
          // send the required amount of ETH
          value: baseCost,
          gasPrice: l1GasPrice,
        },
      });
    
      console.log(`L1 tx hash is ${txReceipt.hash}`);
      console.log('🎉 Transaction sent successfully');
      txReceipt.wait(1);
    

Full example

Create a new script file in the scripts folder called l1tol2tx.ts, and copy/paste the full script.

touch scripts/l1tol2tx.ts

Made with ❤️ by the ZKsync Community