Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds l1-l2 how to guide #43

Merged
merged 10 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
uF4No marked this conversation as resolved.
Show resolved Hide resolved
```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);
uF4No marked this conversation as resolved.
Show resolved Hide resolved
```
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]);
uF4No marked this conversation as resolved.
Show resolved Hide resolved
```
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;
});
```
::
uF4No marked this conversation as resolved.
Show resolved Hide resolved
::
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
Loading