1. Prerequisites
- You have Bun installed.
- A funded L1 wallet with ETH for both the deposit amount and L1 gas fees
Use a test network like Sepolia for experimentation.
2. Installation
Choose your adapter and install the SDK + adapter package:bun install @dutterbutter/zksync-sdk viem dotenv
bun install @dutterbutter/zksync-sdk ethers dotenv
.env file in your project root:
# Your funded L1 private key (0x + 64 hex)
PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
# RPC endpoints
L1_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_ID
L2_RPC_URL=ZKSYNC-OS-TESTNET-RPC
Never commit your
.env file to source control.3. Write the deposit script
import 'dotenv/config';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem';
import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const L1_RPC_URL = process.env.L1_RPC_URL;
const L2_RPC_URL = process.env.L2_RPC_URL;
async function main() {
if (!PRIVATE_KEY || !L1_RPC_URL || !L2_RPC_URL) {
throw new Error('Please set PRIVATE_KEY, L1_RPC_URL, and L2_RPC_URL in your .env file');
}
// 1. Set up clients
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
const l1 = createPublicClient({ transport: http(L1_RPC_URL) });
const l2 = createPublicClient({ transport: http(L2_RPC_URL) });
const l1Wallet = createWalletClient({ account, transport: http(L1_RPC_URL) });
// 2. Initialize the SDK
const client = createViemClient({ l1, l2, l1Wallet });
const sdk = createViemSdk(client);
console.log('Wallet balances:');
console.log(' L1:', await l1.getBalance({ address: account.address }));
console.log(' L2:', await l2.getBalance({ address: account.address }));
// 3. Perform the deposit
console.log('Sending deposit transaction...');
const depositHandle = await sdk.deposits.create({
token: ETH_ADDRESS,
amount: parseEther('0.001'),
to: account.address,
});
console.log(`L1 transaction hash: ${depositHandle.l1TxHash}`);
console.log('Waiting for confirmation on L1...');
const l1Receipt = await sdk.deposits.wait(depositHandle, { for: 'l1' });
console.log(`✔️ Confirmed on L1 at block ${l1Receipt?.blockNumber}`);
console.log('Waiting for execution on L2...');
const l2Receipt = await sdk.deposits.wait(depositHandle, { for: 'l2' });
console.log(`✔️ Executed on L2 at block ${l2Receipt?.blockNumber}`);
console.log('Deposit complete ✅');
console.log('Updated balances:');
console.log(' L1:', await l1.getBalance({ address: account.address }));
console.log(' L2:', await l2.getBalance({ address: account.address }));
}
main().catch((err) => {
console.error('An error occurred:', err);
process.exit(1);
});
import 'dotenv/config'; // Load environment variables from .env
import { JsonRpcProvider, Wallet, parseEther } from 'ethers';
import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers';
import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const L1_RPC_URL = process.env.L1_RPC_URL;
const L2_RPC_URL = process.env.L2_RPC_URL;
async function main() {
if (!PRIVATE_KEY || !L1_RPC_URL || !L2_RPC_URL) {
throw new Error('Please set your PRIVATE_KEY, L1_RPC_URL, and L2_RPC_URL in a .env file');
}
// 1. SET UP PROVIDERS AND SIGNER
// The SDK needs connections to both L1 and L2 to function.
const l1Provider = new JsonRpcProvider(L1_RPC_URL);
const l2Provider = new JsonRpcProvider(L2_RPC_URL);
const signer = new Wallet(PRIVATE_KEY, l1Provider);
// 2. INITIALIZE THE SDK CLIENT
// The client is the low-level interface for interacting with the API.
const client = await createEthersClient({
l1Provider,
l2Provider,
signer,
});
const sdk = createEthersSdk(client);
const L1balance = await l1.getBalance({ address: signer.address });
const L2balance = await l2.getBalance({ address: signer.address });
console.log('Wallet balance on L1:', L1balance);
console.log('Wallet balance on L2:', L2balance);
// 3. PERFORM THE DEPOSIT
// The create() method prepares and sends the transaction.
// The wait() method polls until the transaction is complete.
console.log('Sending deposit transaction...');
const depositHandle = await sdk.deposits.create({
token: ETH_ADDRESS,
amount: parseEther('0.001'), // 0.001 ETH
to: account.address,
});
console.log(`L1 transaction hash: ${depositHandle.l1TxHash}`);
console.log('Waiting for the deposit to be confirmed on L1...');
// Wait for L1 inclusion
const l1Receipt = await sdk.deposits.wait(depositHandle, { for: 'l1' });
console.log(`Deposit confirmed on L1 in block ${l1Receipt?.blockNumber}`);
console.log('Waiting for the deposit to be executed on L2...');
// Wait for L2 execution
const l2Receipt = await sdk.deposits.wait(depositHandle, { for: 'l2' });
console.log(`Deposit executed on L2 in block ${l2Receipt?.blockNumber}`);
console.log('Deposit complete! ✅');
const L1balanceAfter = await l1.getBalance({ address: signer.address });
const L2balanceAfter = await l2.getBalance({ address: signer.address });
console.log('Wallet balance on L1 after:', L1balanceAfter);
console.log('Wallet balance on L2 after:', L2balanceAfter);
}
main().catch((error) => {
console.error('An error occurred:', error);
process.exit(1);
});
4. Run it
Execute the script usingbun.
bun run deposit/viem.ts
bun run deposit/ethers.ts
5. Troubleshooting
- Insufficient funds on L1: Ensure enough ETH for the deposit and L1 gas.
- Invalid PRIVATE_KEY: Must be 0x + 64 hex chars.
- Stuck at wait(…,
{ for: 'l2' }): Verify L2_RPC_URL and network health; check sdk.deposits.status(handle) to see the current phase. - ERC-20 deposits: Make sure you have an L1 token deployed and a sufficient balance.