-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
1ae6d4b
commit 29db882
Showing
2 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
325 changes: 325 additions & 0 deletions
325
content/tutorials/how-to-send-l1-l2-transaction/10.index.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |