diff --git a/content/tutorials/how-to-send-l1-l2-transaction/10.index.md b/content/tutorials/how-to-send-l1-l2-transaction/10.index.md new file mode 100644 index 00000000..9adbb0a4 --- /dev/null +++ b/content/tutorials/how-to-send-l1-l2-transaction/10.index.md @@ -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(); + const l2provider = new Provider(); + 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(, , 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(); + 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: , + 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. diff --git a/content/tutorials/how-to-send-l1-l2-transaction/_dir.yml b/content/tutorials/how-to-send-l1-l2-transaction/_dir.yml new file mode 100644 index 00000000..5d1dd230 --- /dev/null +++ b/content/tutorials/how-to-send-l1-l2-transaction/_dir.yml @@ -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