L1->L2 transaction Local
Requirements
Before continuing, make sure you have the following:
Set up
Create a new project using zksync-cli
:
npx zksync-cli create cross-chain-tx --template hardhat_solidity
For the private key prompt, you can use one of the pre-configured rich wallets like the example below:
? Private key of the wallet responsible for deploying contracts (optional)
0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110
Next, you need to configure the ZKsync CLI to use a Dockerized node.
npx zksync-cli dev config
Then, open Docker desktop so Docker is running in background, and start the nodes:
npx zksync-cli dev start
Once the node is running, change the default network in your hardhat.config.ts
file to dockerizedNode
.
defaultNetwork: 'dockerizedNode',
Let's use the default Greeter
contract in the template to use for testing.
Deploy the Greeter
contract using the commands below:
npm run compile
npm run deploy
Save the contract address to use later.
Step-by-step
Sending an L1->L2 transaction requires following these steps:
- Initialise the providers and the wallet that will send the transaction:
const [wallet] = await ethers.getWallets(); // Initialize the L2 provider. const l2Provider = ethers.providerL2; // Initialize the L1 provider. // eslint-disable-next-line @typescript-eslint/no-explicit-any const networkInfo = network as any; const l1RPCEndpoint = networkInfo.config.ethNetwork === 'http://localhost:8545' ? networkInfo.config.ethNetwork : '<YOUR_L1_RPC_ENDPOINT>'; const l1Provider = new ethers.Provider(l1RPCEndpoint);
- Retrieve the current gas price on L1.
// retrieve L1 gas price const l1GasPrice = await l1Provider.getGasPrice(); console.log(`L1 gasPrice ${ethers.formatEther(l1GasPrice)} ETH`);
- Populate the transaction that you want to execute on L2:
const contract = new ethers.Contract(L2_CONTRACT_ADDRESS, GREETER_ABI_JSON.abi, wallet); const message = `Message sent from L1 at ${new Date().toUTCString()}`; // populate tx object const tx = await contract.setGreeting.populateTransaction(message);
- Retrieve the gas limit for sending the populated transaction to L2 using
estimateGasL1()
. This function calls thezks_estimateGasL1ToL2
RPC method under the hood:// Estimate gas limit for L1-L2 tx const l2GasLimit = await l2Provider.estimateL1ToL2Execute({ contractAddress: L2_CONTRACT_ADDRESS, calldata: tx.data, caller: wallet.address, }); console.log(`L2 gasLimit ${l2GasLimit.toString()}`);
- Calculate the total transaction fee to cover the cost of sending the
transaction on L1 and executing the transaction on L2 using
getBaseCost
from theWallet
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.
const baseCost = await wallet.getBaseCost({ // L2 computation gasLimit: l2GasLimit, // L1 gas price gasPrice: l1GasPrice, }); console.log(`Executing this transaction will cost ${ethers.formatEther(baseCost)} ETH`);
- Retrieves the ZKsync BridgeHub contract address by calling
- Encode the transaction calldata:
const iface = new ethers.Interface(GREETER_ABI_JSON.abi); const calldata = iface.encodeFunctionData('setGreeting', [message]);
- 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.
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, }, }); console.log(`L1 tx hash is ${txReceipt.hash}`); console.log('🎉 Transaction sent successfully'); txReceipt.wait(1);
- Retrieves the Zksync BridgeHub contract address calling
Full example
Create a new script file in the scripts
folder called l1tol2tx.ts
, and copy/paste the full script.
touch scripts/l1tol2tx.ts
Update the L2_CONTRACT_ADDRESS
variable with your deployed Greeter
contract address.
Next, execute the script by running:
npx hardhat run scripts/l1tol2tx.ts
You should see the following output:
L1 gasPrice 0.00000000321159928 ETH
L2 gasLimit 0xa4910
Executing this transaction will cost 0.000195576293213424 ETH
L1 tx hash is 0x69a382f7157772f24984c4985e81258e7d5dfc2f0ba762a6cff41304e8987d9c
🎉 Transaction sent successfully
This indicates the transaction is submitted to the L1 and will later on be executed on L2.
To verify this, wait until the transaction is finalized, and try running the interact.ts
script.
You can remove the second part of the script so that it just checks the value of the greet
function.
npm run interact
You should see in the logs your message sent from the L1.
Running script to interact with contract 0x543A5fBE705d040EFD63D9095054558FB4498F88
Current message is: Message sent from L1 at <CURRENT_DATE>