Getting Started

Getting started with zksync2-rust

Concept

While most of the existing SDKs should work out of the box, deploying smart contracts or using unique ZKsync features, like paying fees in other tokens, requires providing additional fields to those that Ethereum transactions have by default.

To provide easy access to all the features of ZKsync Era, the zksync-web3-rs Rust SDK was created, which builds upon the ethers library. This section serves as its reference.

Adding dependencies

Add the following dependencies to your Cargo.toml file:

zksync-web3-rs = "0.1.1"

Consider adding tokio as dependency since we are using a lot of async/await functions. If this example is meant to be done in the main function the #[tokio::main] annotation is needed.

tokio = { version = "1", features = ["macros", "process"] }
#[tokio::main(flavor = "current_thread")]
async fn main() {
    // ...
}

Connecting to ZKsync Era

To interact with the ZKsync Era network users need to know the endpoint of the operator node. In this tutorial, we will be using the localnet from Dockerized L1 - L2 Nodes. The localnet runs both an Ethereum node (L1) on port 8545 and an Era node (L2) on port 3050. You can connect to the ZKsync Era network using the following code:

use zksync_web3_rs::providers::{Http, Middleware, Provider};
// The trait `ZKSProvider` extends `Provider<Http>` with specific methods for ZKsync Era.
use zksync_web3_rs::zks_provider::ZKSProvider;

static L2_URL: &str = "http://localhost:3050";

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let l2_provider: Provider<Http> =
        Provider::try_from(L2_URL).expect("could not instantiate HTTP Provider");

    // Test the connection with the ZKsync Era node:
    let l2_chain_id = l2_provider
        .get_chainid()
        .await
        .expect("Could not retrieve L2 chain id");
    println!("l2_chain_id = {l2_chain_id}");

    let l1_chain_id = l2_provider
        .get_l1_chain_id()
        .await
        .expect("Could not retrieve L1 chain id from ZKsync Era node");
    println!("l1_chain_id = {l1_chain_id}");
}

Which should print:

l2_chain_id = 270
l1_chain_id = 9

Here, the trait ZKSProvider extended the Provider<Http> struct adding ZKsync Era specific functionality. In this case, the method .get_chainid() comes from ethers while the method get_l1_chain_id comes from the ZKSProvider and hits an era-specific API.

Connecting to Ethereum layer-1

To perform operations that interact with both layers, we will also need to instantiate a provider for the Ethereum layer-1 associated with our ZKsync Era blockchain.

static L1_URL: &str = "http://localhost:8545";

let l1_provider =
    Provider::<Http>::try_from(L1_URL).expect("Could not instantiate L1 Provider");

Creating a wallet

To control an account in ZKsync, use the zksync_web3_rs::ZKSWallet struct. It can sign transactions with keys stored in zksync_web3_rs::signers::{LocalWallet, Signer} and send transactions to the ZKsync network using a Provider.

use std::str::FromStr;
use zksync_web3_rs::signers::LocalWallet;
use zksync_web3_rs::signers::Signer;
use zksync_web3_rs::ZKSWallet;

static WALLET_1_PRIVATE_KEY: &str =
    "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";

let zk_wallet_1 = {
    let l2_wallet = LocalWallet::from_str(WALLET_1_PRIVATE_KEY)
        .expect("Invalid private key")
        .with_chain_id(l2_chain_id.as_u64());

    ZKSWallet::new(
        l2_wallet,
        None,
        Some(l2_provider.clone()),
        Some(l1_provider.clone()),
    )
    .unwrap()
};

Checking ZKsync account balance

You can use ZKSWallet eth_balance and era_balance to get L1 and L2 balances respectively:

println!(
    "Wallet-1 balance (L1): {}",
    zk_wallet_1.eth_balance().await.unwrap()
);
println!(
    "Wallet-1 balance (L2): {}",
    zk_wallet_1.era_balance().await.unwrap()
);

Depositing funds

Let's deposit 1.0 ETH to our ZKsync account.

use zksync_web3_rs::utils::parse_units;
use zksync_web3_rs::zks_wallet::DepositRequest;

let deposit_transaction_hash = {
    let amount = parse_units("0.1", "ether").unwrap();
    let request = DepositRequest::new(amount.into());
    zk_wallet_1
        .deposit(&request)
        .await
        .expect("Failed to perform deposit transaction")
};

println!("Deposit transaction hash: {:?}", deposit_transaction_hash);
Each token inside ZKsync has an address. If ERC-20 tokens are being bridged, you should supply the token's L1 address by calling the token method from DepositRequest, or zero address (0x0000000000000000000000000000000000000000) if you want to deposit ETH. Note, that for the ERC-20 tokens the address of their corresponding L2 token will be different from the one on Ethereum.

After the transaction is submitted to the Ethereum node, its status can be tracked using the transaction hash:

let deposit_l1_receipt = l1_provider
    .get_transaction_receipt(deposit_transaction_hash)
    .await
    .unwrap()
    .unwrap();

println!(
    "Deposit L1 receipt status: {}",
    deposit_l1_receipt.status.unwrap()
);

Then, we can check the resulting account balances:

println!(
    "Wallet-1 balance (L1): {}",
    zk_wallet_1.eth_balance().await.unwrap()
);
println!(
    "Wallet-1 balance (L2): {}",
    zk_wallet_1.era_balance().await.unwrap()
);

Performing a transfer

Now, let's create a second wallet and transfer some funds into it. Note that it is possible to send assets to any fresh Ethereum account, without preliminary registration!

static WALLET_2_PRIVATE_KEY: &str =
    "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3";

let zk_wallet_2 = {
    let l2_wallet = LocalWallet::from_str(WALLET_2_PRIVATE_KEY)
        .expect("Invalid private key")
        .with_chain_id(l2_chain_id.as_u64());

    ZKSWallet::new(
        l2_wallet,
        None,
        Some(l2_provider.clone()),
        Some(l1_provider.clone()),
    )
    .unwrap()
};

We can check it's balances to verify that it has no funds:

println!(
    "L1 balance for the new wallet: {}",
    zk_wallet_2.eth_balance().await.unwrap()
);
println!(
    "L2 balance for the new wallet: {}",
    zk_wallet_2.era_balance().await.unwrap()
);
L1 balance for the new wallet: 0
L2 balance for the new wallet: 0

Let's transfer 1 ETH to the new account:

The transfer method from the ZSKWallet structure is a helper method that enables transferring ETH or any ERC-20 token within a single interface on the layer 2.

use zksync_web3_rs::zks_wallet::TransferRequest;

let transfer_transaction_hash = {
    let transfer_amount = parse_units("0.05", "ether").unwrap();
    let transfer_request = TransferRequest::new(transfer_amount.into())
        .to(zk_wallet_2.l2_address())
        .from(zk_wallet_1.l2_address());
    zk_wallet_1.transfer(&transfer_request, None).await.unwrap()
};
Transfer transaction hash: 0x84d3…8ced

After the transfer transaction is submitted to the ZKsync Era node, its status can be tracked using the transaction hash:

let transfer_receipt = l2_provider
    .get_transaction_receipt(transfer_transaction_hash)
    .await
    .unwrap()
    .unwrap();

println!(
    "Transfer receipt status: {}",
    transfer_receipt.status.unwrap()
);
Transfer receipt status: 1

Withdrawing funds

use zksync_web3_rs::zks_wallet::WithdrawRequest;

let withdraw_transaction_hash_l2 = {
    let amount = parse_units("0.025", "ether").unwrap();
    let request = WithdrawRequest::new(amount.into()).to(zk_wallet_2.l1_address());
    zk_wallet_2.withdraw(&request).await.unwrap()
};

let withdraw_receipt_l2 = l2_provider
        .wait_for_finalize(withdraw_transaction_hash_l2, None, None)
        .await
        .unwrap();

Assets will be withdrawn to the target wallet after the validity proof of the ZKsync Era block with this transaction is generated and verified by the mainnet contract.

It is possible to wait until the validity proof verification is complete using the finalize_withdraw method:

let withdraw_transaction_hash_l1 = zk_wallet_2.finalize_withdraw(withdraw_transaction_hash_l2).await.unwrap();

// Then, get the transaction receipt:
let withdraw_receipt_l1 = zk_wallet_2.get_eth_provider().unwrap().get_transaction_receipt(withdraw_transaction_hash_l1).await.unwrap().unwrap();
For development and testing, it is recommended to use burner wallets. Avoid using real private keys to prevent security risks.

Made with ❤️ by the ZKsync Community