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..eb64eed0 --- /dev/null +++ b/content/tutorials/how-to-send-l1-l2-transaction/10.index.md @@ -0,0 +1,329 @@ +--- +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 continue, 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 assume a `Greeter.sol` contract is deployed + on ZKsync ([like this one](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). +- Account with ETH in L1 to pay the gas fees. Use any of the [faucets listed here](/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..; + ``` + +## Step-by-step + +Sending an L1->L2 transaction requires following these steps: + +1. Initialise the L1 and L2 providers and 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_PRIV_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); +// 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: + +```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: + 1. Retrieves the Zksync BridgeHub contract address calling `zks_getBridgehubContract`. + 2. Populates the L1-L2 transaction. + 3. Sends the transaction to `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 next script: + +::drop-panel + ::panel{label="send-l1-l2-tx.ts"} + + ```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: [ + { + internalType: "string", + name: "_greeting", + type: "string", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + 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"; + const L2_RPC_ENDPOINT = "https://sepolia.era.zksync.dev"; + + const WALLET_PRIV_KEY = process.env.WALLET_PRIV_KEY || ""; + + if (!WALLET_PRIV_KEY) { + throw new Error("Wallet private key is not configured in env file"); + } + + // 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); + + // console.log(`L1 Balance is ${await wallet.getBalanceL1()}`); + console.log(`L2 Balance is ${await wallet.getBalance()}`); + + // 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