# Getting started

In this tutorial, we will demonstrate how to:

  1. Connect to the zkSync network.
  2. Deposit assets from Ethereum into zkSync.
  3. Make transfers.
  4. Withdraw funds back to Ethereum.

# Adding dependencies

Put the following line in your Cargo.toml:

zksync = { git = "https://github.com/matter-labs/zksync", version = "0.3.0" }

Unfortunately, the SDK is not currently published on crates.io; thus specifying the dependency from the repository is the only option.

# Connecting to the zkSync network

To interact with the zkSync network, users need to know the endpoint of the operator node.

use zksync::{Provider, Network};

let provider = Provider::new(Network::Goerli);

# Instantiating an Ethereum signer

An Ethereum signer is mandatory for sending both L1 and L2 transactions since L2 transactions require an Ethereum signature as part of 2-factor authentication. It is possible to create a wallet without an Ethereum private key, but this wallet will only be able to perform read requests to the zkSync server.

The Ethereum signer is represented by the EthereumSigner trait from the zksync_eth_signer crate.

By default, this trait has two implementations: PrivateKeySigner and JsonRpcSigner.

The PrivateKeySigner implementation assumes that you own your private key and can use it directly:

use zksync_eth_signer::PrivateKeySigner;

let signer = PrivateKeySigner::new(YOUR_PRIVATE_KEY);

The JsonRpcSigner implementation may be used if the private key is managed by software that exposes a personal Web3 API:

use zksync_eth_signer::JsonRpcSigner;

let address = JsonRpcSigner::new("http://127.0.0.1:8545", None, None, None);

The arguments are:

  • rpc_addr: The address of the wallet RPC server.
  • address_or_index: The identifier of the wallet to be used. If None, the first available wallet will be chosen.
  • signer_type: Indicates whether the signer adds the \x19Ethereum... prefix to signed messages. If None, the signer type will be deduced automatically by signing an additional message.
  • password_to_unlock: Sets the wallet password if it's required.

Also, if your software uses a custom signer, you can always provide your implementation of the EthereumSigner trait.

# Creating a wallet

To control your account in zkSync, use the zksync::Wallet object. It can sign transactions with keys stored in zksync::Signer and send transactions to the zkSync network by using zksync::Provider.

In order to create a Wallet object, you have to initialize credentials that will store the private keys for the wallet.

A WalletCredentials object can be created from a seed byte array, an Ethereum private key, or any EthereumSigner implementation:

use zksync::{WalletCredentials, Network};

let address = ...; // Not essential for this example.

// Generate from seed. A zkSync private key will be initialized, an Ethereum private key will not.
let cred_1 = WalletCredentials::from_seed(address, &[0u8; 32]);
// Generate from a zkSync private key only. An Ethereum private key will not be set.
let cred_2 = WalletCredentials::from_pk(address, ZKSYNC_PRIVATE_KEY, None);
// Generate from both a zkSync and an Ethereum private key.
let cred_3 = WalletCredentials::from_pk(address, ZKSYNC_PRIVATE_KEY, Some(ETH_PRIVATE_KEY));
// Generate from a custom Ethereum signer. The Ethereum private key is assumed to be accessible through the provided signer,
// A zkSync private key will be derived from the Ethereum private key using a deterministic algorithm.
let cred_4 = WalletCredentials::from_eth_signer(address, custom_signer, Network::Goerli);

Once the WalletCredentials and Provider objects are set, you can create a wallet:

use zksync::Wallet;

// This method is asynchronous since it retrieves an account ID from the server in the constructor.
let wallet = Wallet::new(provider, cred).await;

# Depositing assets from Ethereum into zkSync

Depositing requires the WalletCredentials object to be created with access to the Ethereum signer.

We are going to deposit 1.0 ETH to our zkSync account.

let one_ether = U256::from(10).pow(18.into());

// The address of the server providing access to the API of the Ethereum node, for example, Infura or a local node.
// This address is required in order to interact with the Ethereum blockchain.
let web3_address = "http://127.0.0.1:8545";

let ethereum = wallet.ethereum(web3_address).await?;
let deposit_tx_hash = ethereum.deposit("ETH", one_ether, wallet.address()).await?;

"ETH" stands for native ether. To transfer a supported ERC20 token, use the ERC20 address or ERC20 symbol instead of "ETH".

After the transaction is submitted to the Ethereum node, we can track its status using the returned object:

use zksync::ethereum::PriorityOpHolder;
let receipt = ethereum.wait_for_tx(deposit_tx_hash).await?;

let deposit_op = receipt.priority_op().expect("Transaction receipt should hold priority operation data");

// Now we can query the zkSync server for information about deposit execution.
let deposit_info = wallet.provider.ethop_info(deposit_op.serial_id as u32);

# Unlocking a zkSync account

To control assets in the zkSync network, an account must register its public key.

if !wallet.is_signing_key_set().await? {
    assert!(wallet.account_id().is_none());

    let change_pubkey = wallet
        .start_change_pubkey()
        .fee_token("ETH")?
        .send()
        .await?

    let change_pubkey_receipt = change_pubkey.wait_for_commit().await?;
    assert_eq!(change_pubkey_receipt.success, Some(true));

    // After setting the signing key, we have to update the account ID stored in the wallet.
    wallet.update_account_id().await?;
    assert!(wallet.account_id().is_some());
}

# Checking a zkSync account's balance

use zksync::types::BlockStatus;

// The committed state is not final yet.
let committedETHBalance = wallet.getBalance(BlockStatus::Committed, 'ETH').await?;

// The verified state is final.
let verifiedETHBalance = wallet.getBalance(BlockStatus::Verified, 'ETH').await?;

To list all tokens of this account, use account_info:

let info = wallet.account_info().await?;
let committed_eth_balance = info.committed.balances.get("ETH");
let verified_dai_balance = info.balance.balances.get("DAI");

# Making a transfer in zkSync

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

let another_cred = WalletCredentials::from_seed(address, &[1u8; 32]);
let another_wallet = Wallet::new(provider, another_cred).await;

We are going to transfer 0.5 ETH to another account. The fee will be set automatically to the least possible fee accepted by the server.

Note that the SDK may round down the transferred amount or fee to the closest supported amount because the precision of transfers in zkSync is limited (see docs below).

However, you can provide amount and fee values that won't be rounded: use the methods .amount_exact(..) and .fee_exact(..) and ensure that your amount and fee are packable via zksync::utils::is_token_amount_packable and zksync::utils::is_fee_amount_packable. Rounding to the closest packable amount can also be performed manually via the zksync::utils::closest_packable_token_amount and zksync::utils::closest_packable_fee_amount functions. An attempt to send a transaction with either an amount or a fee that isn't packable will result in the transaction being rejected by the server.

use zksync::utils::{closest_packable_fee_amount, closest_packable_token_amount};

// "one_ether" is the same variable we declared above.
let transfer_amount = one_ether / 2u64.into();

let transfer_handle = wallet
  .start_transfer()
  .token("ETH")
  .amount(transfer_amount)
  .to(another_wallet.address())
  .send()
  .await?;

For more control over the transaction flow, you can manually request the fee and check whether it's appropriate.

let fee = wallet
  .provider
  .get_tx_fee(TxFeeTypes::Transfer, another_wallet.address(), "ETH")
  .await?
  .total_fee;
assert!(is_fee_amount_packable(&fee));

let transfer_handle = wallet
  .start_transfer()
  .token("ETH")
  .amount(transfer_amount)
  .fee_exact(fee)?
  .to(another_wallet.address())
  .send()
  .await?;

To track the status of this transaction:

let transfer_receipt = transfer_handle.wait_for_commit().await?;

# Withdrawing funds back to Ethereum

let withdraw_amount = one_ether / 10u64.into();

let withdraw_handle = wallet
  .start_withdraw()
  .token("ETH")
  .amount(withdraw_amount)
  .to(wallet.address()) // Withdraw to the same L1 address.
  .send()
  .await?;

Assets will be withdrawn to the target wallet after the zero-knowledge proof of the zkSync block with this operation is generated and verified by the mainnet contract.

We can wait until ZKP verification is complete:

withdraw_handle.wait_for_verify().await?;