Skip to content

Commit

Permalink
feat: adds l1-l2 how to guide (#43)
Browse files Browse the repository at this point in the history
* feat: l1-l2 how to guide

* fix: broken link

* fix: linting

* fix: rename title

* fix: format steps

* fix: pk notes

* Apply suggestions from code review

Co-authored-by: Sabrina <[email protected]>

* fix: pr review comments

* Update content/tutorials/how-to-send-l1-l2-transaction/10.index.md

Co-authored-by: Sabrina <[email protected]>

---------

Co-authored-by: Sabrina <[email protected]>
  • Loading branch information
uF4No and itsacoyote authored Aug 21, 2024
1 parent 1ae6d4b commit 29db882
Show file tree
Hide file tree
Showing 2 changed files with 345 additions and 0 deletions.
325 changes: 325 additions & 0 deletions content/tutorials/how-to-send-l1-l2-transaction/10.index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
---
title: Send a transaction from L1 to L2
description: This how-to guide explains how to send a transaction from Ethereum that interacts with a contract deployed on ZKsync.
---

The [ZKsync Era smart contracts](https://github.com/matter-labs/era-contracts/tree/main/l1-contracts/contracts/zksync)
allow a sender to request transactions on Ethereum (L1) and pass data to a contract deployed on ZKsync Era (L2).

In this example we'll send a transaction to an L2 contract from L1 using `zksync-ethers`, which provides helper methods to simplify the process.

## Common use cases

Along with ZKsync Era's built-in censorship resistance that requires multi-layer interoperability,
there are some common use cases that need L1 to L2 transaction functionality, such as:

- Custom bridges.
- Multi-layer governing smart contracts.

## Requirements

Before continuing, make sure you have the following:

- Destination contract ABI: in order to execute a transaction on a contract on L2,
you'll need the contract's ABI. For this example we'll use a `Greeter.sol` contract that is deployed
on [ZKsync Sepolia testnet](https://sepolia.explorer.zksync.io/address/0x543A5fBE705d040EFD63D9095054558FB4498F88)).
- L1 RPC endpoint: to broadcast the L1->L2 transaction to the network.
You can find [public Sepolia RPC endpoints in Chainlist](https://chainlist.org/chain/11155111).
- L1 Account with ETH to pay the gas fees. Use any of the [listed Sepolia faucets](https://docs.zksync.io/ecosystem/network-faucets#sepolia-faucets).

## Set up

1. Create a project folder and `cd` into it

```sh
mkdir cross-chain-tx
cd cross-chain-tx
```

1. Initialise the project:

::code-group

```bash [npm]
npm init -y
```

```bash [yarn]
yarn init -y
```

::

1. Install the required dependencies:

::code-group

```bash [npm]
npm install -D zksync-ethers ethers typescript dotenv @matterlabs/zksync-contracts @types/node
```

```bash [yarn]
yarn add -D zksync-ethers ethers typescript dotenv @matterlabs/zksync-contracts @types/node
```

::

1. Install `ts-node` globally to execute the scripts that we're going to create:
::code-group
```bash [npm]
npm install -g ts-node
```
```bash [yarn]
yarn global add ts-node
```
::
1. In the root folder, add a `.env` file with the private key of the wallet to use:
```md
WALLET_PRIVATE_KEY=0x..;
```
::callout{icon="i-heroicons-exclamation-triangle"}
Always use a separate wallet with no real funds for development.
Make sure your `.env` file is not pushed to an online repository by adding it to a `.gitignore` file.
::
## Step-by-step
Sending an L1->L2 transaction requires following these steps:
1. Initialise the providers and the wallet that will send the transaction:
```ts
const l1provider = new Provider(<L1_RPC_ENDPOINT>);
const l2provider = new Provider(<L2_RPC_ENDPOINT>);
const wallet = new Wallet(WALLET_PRIVATE_KEY, l2provider, l1provider);
```
1. Retrieve the current gas price on L1.
```ts
// retrieve L1 gas price
const l1GasPrice = await l1provider.getGasPrice();
console.log(`L1 gasPrice ${ethers.utils.formatEther(l1GasPrice)} ETH`);
```
1. Populate the transaction that you want to execute on L2:
```ts
const contract = new Contract(<L2_CONTRACT_ADDRESS>, <ABI>, wallet);
const message = `Message sent from L1 at ${new Date().toUTCString()}`;
// populate tx object
const tx = await contract.populateTransaction.setGreeting(message);
```
1. Retrieve the gas limit for sending the populated transaction to L2 using `estimateGasL1()` from `zksync-ethers` `Provider` class.
This function calls the `zks_estimateGasL1ToL2` RPC method under the hood:
```ts
// Estimate gas limit for L1-L2 tx
const l2GasLimit = await l2provider.estimateGasL1(tx);
console.log(`L2 gasLimit ${l2GasLimit.toString()}`);
```
1. 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.
```ts
const baseCost = await wallet.getBaseCost({
// L2 computation
gasLimit: l2GasLimit,
// L1 gas price
gasPrice: l1GasPrice,
});
console.log(`Executing this transaction will cost ${ethers.utils.formatEther(baseCost)} ETH`);
```
1. Encode the transaction calldata:
```ts
const iface = new ethers.utils.Interface(<ABI>);
const calldata = iface.encodeFunctionData("setGreeting", [message]);
```
1. 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.
```ts
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,
},
});
txReceipt.wait();
```
## Full example
Create a `send-l1-l2-tx.ts` file in the root directory with the following script:
::drop-panel
::panel{label="Click to show L1-L2 full script"}
```ts [send-l1-l2-tx.ts]
import { Contract, Wallet, Provider, types } from "zksync-ethers";
import * as ethers from "ethers";
// load env file
import dotenv from "dotenv";
dotenv.config();
// Greeter contract ABI for example
const ABI = [
{
inputs: [],
name: "greet",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "string",
name: "_greeting",
type: "string",
},
],
name: "setGreeting",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];
// RPC endpoints
const L1_RPC_ENDPOINT = "https://rpc2.sepolia.org"; // Check chainlist.org
const L2_RPC_ENDPOINT = "https://sepolia.era.zksync.dev";
const WALLET_PRIV_KEY = process.env.WALLET_PRIVATE_KEY || "";
if (!WALLET_PRIV_KEY) {
throw new Error("Wallet private key is not configured in env file");
}
// Example Greeter contract on ZKsync Sepolia Testnet
const L2_CONTRACT_ADDRESS = "0x543A5fBE705d040EFD63D9095054558FB4498F88";
async function main() {
console.log(`Running script for L1-L2 transaction`);
// Initialize the wallet.
const l1provider = new Provider(L1_RPC_ENDPOINT);
const l2provider = new Provider(L2_RPC_ENDPOINT);
const wallet = new Wallet(WALLET_PRIV_KEY, l2provider, l1provider);
// retrieve L1 gas price
const l1GasPrice = await l1provider.getGasPrice();
console.log(`L1 gasPrice ${ethers.formatEther(l1GasPrice)} ETH`);
const contract = new Contract(L2_CONTRACT_ADDRESS, ABI, wallet);
const msg = await contract.greet();
console.log(`Message in contract is ${msg}`);
const message = `Message sent from L1 at ${new Date().toUTCString()}`;
let tx = await contract.setGreeting.populateTransaction(message);
tx = {
...tx,
from: wallet.address,
}
// call to RPC method zks_estimateGasL1ToL2 to estimate L2 gas limit
const l1l2GasLimit = await l2provider.estimateGasL1(tx);
console.log(`L2 gasLimit ${BigInt(l1l2GasLimit)}`);
const baseCost = await wallet.getBaseCost({
// L2 computation
gasLimit: l1l2GasLimit,
// L1 gas price
gasPrice: l1GasPrice,
});
console.log(`Executing this transaction will cost ${ethers.formatEther(baseCost)} ETH`);
const iface = new ethers.Interface(ABI);
const calldata = iface.encodeFunctionData("setGreeting", [message]);
const txReceipt = await wallet.requestExecute({
contractAddress: L2_CONTRACT_ADDRESS,
calldata,
l2GasLimit: l1l2GasLimit,
refundRecipient: wallet.address,
overrides: {
// send the required amount of ETH
value: baseCost,
gasPrice: l1GasPrice,
},
});
console.log('txReceipt :>> ', txReceipt);
console.log(`L1 tx hash is ${txReceipt.hash}`);
console.log("🎉 Transaction sent successfully");
txReceipt.wait(1);
}
main()
.then()
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
```
::
::
Execute the script by running:
```bash
ts-node send-l1-l2-tx.ts
```
You should see the following output:
```bash
Running script for L1-L2 transaction
L2 Balance is 1431116173253819600
L1 gasPrice 0.000000003489976197 ETH
Message in contract is Hello world!
L2 gasLimit 17207266
Executing this transaction will cost 0.005052478351484732 ETH
L1 tx hash is :>> 0x088700061730e84c316fa07296839263a420b65237fd0fd6a147b3d76affef76
🎉 Transaction sent successfully
```
This indicates the transaction is submitted to the L1 and will later on be executed on L2.
20 changes: 20 additions & 0 deletions content/tutorials/how-to-send-l1-l2-transaction/_dir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
title: How to send an L1->L2 transaction
authors:
- name: Matter Labs
url: https://matter-labs.io
avatar: https://avatars.githubusercontent.com/u/42489169?s=200&v=4
github_repo: https://github.com/ZKsync-Community-Hub
tags:
- smart contracts
- how-to
- L2-L1 communication
summary: Learn how to send a transaction from Ethereum that to ZKsync
description:
This how-to guide explains how to send a transaction from Ethereum that interacts with a contract deployed on ZKsync.
what_you_will_learn:
- How L1-L2 communication works
- How to send a transaction from ZKsync Ethereum to ZKsync.
updated: 2024-08-18
tools:
- zksync-ethers
- zksync-contracts

0 comments on commit 29db882

Please sign in to comment.