diff --git a/.github/workflows/ci-docs.yaml b/.github/workflows/ci-docs.yaml deleted file mode 100644 index 52fd46f..0000000 --- a/.github/workflows/ci-docs.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: ci-run-docs - -on: - push: - branches: [main] - paths: - - 'docs/**' - - '.github/workflows/docs.yml' - workflow_dispatch: - -# Required for Pages & commenting on PRs -permissions: - contents: read - pages: write - id-token: write - pull-requests: write - -concurrency: - group: pages-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-docs: - name: build-docs 📘 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo (mdBook) - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-mdbook-${{ hashFiles('docs/**') }} - restore-keys: | - ${{ runner.os }}-cargo-mdbook- - - - name: Install mdBook - run: | - if ! command -v mdbook >/dev/null 2>&1; then - cargo install mdbook --version "^0.4" --locked - cargo install mdbook-admonish - fi - - - name: Build book - run: mdbook build docs - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v4 - with: - path: docs/book - - deploy-docs: - name: deploy-docs 📘 - runs-on: ubuntu-latest - needs: build-docs - steps: - - name: Configure Pages - uses: actions/configure-pages@v5 - - - name: Deploy - id: deploy - uses: actions/deploy-pages@v4 - - # Comment the preview URL on PRs - - name: Comment preview link - if: ${{ github.event_name == 'pull_request' }} - uses: actions/github-script@v7 - with: - script: | - const url = `${{ steps.deploy.outputs.page_url }}`; - const body = `📚 **Docs preview** ready: ${url}`; - const {owner, repo} = context.repo; - const issue_number = context.payload.pull_request.number; - - // Upsert a single bot comment - const list = await github.rest.issues.listComments({owner, repo, issue_number}); - const existing = list.data.find(c => c.user?.type === 'Bot' && c.body?.includes('Docs preview')); - if (existing) { - await github.rest.issues.updateComment({owner, repo, comment_id: existing.id, body}); - } else { - await github.rest.issues.createComment({owner, repo, issue_number, body}); - } diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 0000000..5411374 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mintlify + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4981553 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,44 @@ +# ss + +Use the starter kit to get your docs deployed and ready to customize. + +Click the green **Use this template** button at the top of this repo to copy the Mintlify starter kit. The starter kit contains examples with + +- Guide pages +- Navigation +- Customizations +- API reference pages +- Use of popular components + +**[Follow the full quickstart guide](https://starter.mintlify.com/quickstart)** + +## Development + +Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command: + +``` +npm i -g mint +``` + +Run the following command at the root of your documentation, where your `docs.json` is located: + +``` +mint dev +``` + +View your local preview at `http://localhost:3000`. + +## Publishing changes + +Install our GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app) to propagate changes from your repo to your deployment. Changes are deployed to production automatically after pushing to the default branch. + +## Need help? + +### Troubleshooting + +- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI. +- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`. + +### Resources + +- [Mintlify documentation](https://mintlify.com/docs) diff --git a/docs/ai-tools/claude-code.mdx b/docs/ai-tools/claude-code.mdx new file mode 100644 index 0000000..20be6cd --- /dev/null +++ b/docs/ai-tools/claude-code.mdx @@ -0,0 +1,83 @@ +--- +title: 'Claude Code setup' +description: 'Configure Claude Code for your documentation workflow' +icon: 'asterisk' +--- + +Claude Code is Anthropic's official CLI tool. This guide will help you set up Claude Code to help you write and maintain your documentation. + +## Prerequisites + +- Active Claude subscription (Pro, Max, or API access) + +## Setup + +1. Install Claude Code globally: + +```bash +npm install -g @anthropic-ai/claude-code +``` + +2. Navigate to your docs directory. +3. (Optional) Add the `CLAUDE.md` file below to your project. +4. Run `claude` to start. + +## Create `CLAUDE.md` + +Create a `CLAUDE.md` file at the root of your documentation repository to train Claude Code on your specific documentation standards: + +```markdown +# Mintlify documentation + +## Working relationship + +- You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so +- ALWAYS ask for clarification rather than making assumptions +- NEVER lie, guess, or make up information + +## Project context + +- Format: MDX files with YAML frontmatter +- Config: docs.json for navigation, theme, settings +- Components: Mintlify components + +## Content strategy + +- Document just enough for user success - not too much, not too little +- Prioritize accuracy and usability of information +- Make content evergreen when possible +- Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason +- Check existing patterns for consistency +- Start by making the smallest reasonable changes + +## Frontmatter requirements for pages + +- title: Clear, descriptive page title +- description: Concise summary for SEO/navigation + +## Writing standards + +- Second-person voice ("you") +- Prerequisites at start of procedural content +- Test all code examples before publishing +- Match style and formatting of existing pages +- Include both basic and advanced use cases +- Language tags on all code blocks +- Alt text on all images +- Relative paths for internal links + +## Git workflow + +- NEVER use --no-verify when committing +- Ask how to handle uncommitted changes before starting +- Create a new branch when no clear branch exists for changes +- Commit frequently throughout development +- NEVER skip or disable pre-commit hooks + +## Do not + +- Skip frontmatter on any MDX file +- Use absolute URLs for internal links +- Include untested code examples +- Make assumptions - always ask for clarification +``` diff --git a/docs/ai-tools/cursor.mdx b/docs/ai-tools/cursor.mdx new file mode 100644 index 0000000..6ec6152 --- /dev/null +++ b/docs/ai-tools/cursor.mdx @@ -0,0 +1,423 @@ +--- +title: 'Cursor setup' +description: 'Configure Cursor for your documentation workflow' +icon: 'arrow-pointer' +--- + +Use Cursor to help write and maintain your documentation. This guide shows how to configure Cursor for better results on technical writing tasks and using Mintlify components. + +## Prerequisites + +- Cursor editor installed +- Access to your documentation repository + +## Project rules + +Create project rules that all team members can use. In your documentation repository root: + +```bash +mkdir -p .cursor +``` + +Create `.cursor/rules.md`: + +````markdown +# Mintlify technical writing rule + +You are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices. + +## Core writing principles + +### Language and style requirements + +- Use clear, direct language appropriate for technical audiences +- Write in second person ("you") for instructions and procedures +- Use active voice over passive voice +- Employ present tense for current states, future tense for outcomes +- Avoid jargon unless necessary and define terms when first used +- Maintain consistent terminology throughout all documentation +- Keep sentences concise while providing necessary context +- Use parallel structure in lists, headings, and procedures + +### Content organization standards + +- Lead with the most important information (inverted pyramid structure) +- Use progressive disclosure: basic concepts before advanced ones +- Break complex procedures into numbered steps +- Include prerequisites and context before instructions +- Provide expected outcomes for each major step +- Use descriptive, keyword-rich headings for navigation and SEO +- Group related information logically with clear section breaks + +### User-centered approach + +- Focus on user goals and outcomes rather than system features +- Anticipate common questions and address them proactively +- Include troubleshooting for likely failure points +- Write for scannability with clear headings, lists, and white space +- Include verification steps to confirm success + +## Mintlify component reference + +### Callout components + +#### Note - Additional helpful information + + +Supplementary information that supports the main content without interrupting flow + + +#### Tip - Best practices and pro tips + + +Expert advice, shortcuts, or best practices that enhance user success + + +#### Warning - Important cautions + + +Critical information about potential issues, breaking changes, or destructive actions + + +#### Info - Neutral contextual information + + +Background information, context, or neutral announcements + + +#### Check - Success confirmations + + +Positive confirmations, successful completions, or achievement indicators + + +### Code components + +#### Single code block + +Example of a single code block: + +```javascript config.js +const apiConfig = { + baseURL: 'https://api.example.com', + timeout: 5000, + headers: { + Authorization: `Bearer ${process.env.API_TOKEN}`, + }, +}; +``` + +#### Code group with multiple languages + +Example of a code group: + + +```javascript Node.js +const response = await fetch('/api/endpoint', { + headers: { Authorization: `Bearer ${apiKey}` } +}); +``` + +```python Python +import requests +response = requests.get('/api/endpoint', + headers={'Authorization': f'Bearer {api_key}'}) +``` + +```curl cURL +curl -X GET '/api/endpoint' \ + -H 'Authorization: Bearer YOUR_API_KEY' +``` + + + +#### Request/response examples + +Example of request/response documentation: + + +```bash cURL +curl -X POST 'https://api.example.com/users' \ + -H 'Content-Type: application/json' \ + -d '{"name": "John Doe", "email": "john@example.com"}' +``` + + + +```json Success +{ + "id": "user_123", + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-15T10:30:00Z" +} +``` + + +### Structural components + +#### Steps for procedures + +Example of step-by-step instructions: + + + + Run `npm install` to install required packages. + + + Verify installation by running `npm list`. + + + + + Create a `.env` file with your API credentials. + + ```bash + API_KEY=your_api_key_here + ``` + + + Never commit API keys to version control. + + + + +#### Tabs for alternative content + +Example of tabbed content: + + + + ```bash + brew install node + npm install -g package-name + ``` + + + + ```powershell + choco install nodejs + npm install -g package-name + ``` + + + + ```bash + sudo apt install nodejs npm + npm install -g package-name + ``` + + + +#### Accordions for collapsible content + +Example of accordion groups: + + + + - **Firewall blocking**: Ensure ports 80 and 443 are open + - **Proxy configuration**: Set HTTP_PROXY environment variable + - **DNS resolution**: Try using 8.8.8.8 as DNS server + + + + ```javascript + const config = { + performance: { cache: true, timeout: 30000 }, + security: { encryption: 'AES-256' } + }; + ``` + + + +### Cards and columns for emphasizing information + +Example of cards and card groups: + + +Complete walkthrough from installation to your first API call in under 10 minutes. + + + + + Learn how to authenticate requests using API keys or JWT tokens. + + + + Understand rate limits and best practices for high-volume usage. + + + +### API documentation components + +#### Parameter fields + +Example of parameter documentation: + + +Unique identifier for the user. Must be a valid UUID v4 format. + + + +User's email address. Must be valid and unique within the system. + + + +Maximum number of results to return. Range: 1-100. + + + +Bearer token for API authentication. Format: `Bearer YOUR_API_KEY` + + +#### Response fields + +Example of response field documentation: + + +Unique identifier assigned to the newly created user. + + + +ISO 8601 formatted timestamp of when the user was created. + + + +List of permission strings assigned to this user. + + +#### Expandable nested fields + +Example of nested field documentation: + + +Complete user object with all associated data. + + + + User profile information including personal details. + + + + User's first name as entered during registration. + + + + URL to user's profile picture. Returns null if no avatar is set. + + + + + + +### Media and advanced components + +#### Frames for images + +Wrap all images in frames: + + +Main dashboard showing analytics overview + + + +Analytics dashboard with charts + + +#### Videos + +Use the HTML video element for self-hosted video content: + + + +Embed YouTube videos using iframe elements: + + + +#### Tooltips + +Example of tooltip usage: + + +API + + +#### Updates + +Use updates for changelogs: + + +## New features +- Added bulk user import functionality +- Improved error messages with actionable suggestions + +## Bug fixes + +- Fixed pagination issue with large datasets +- Resolved authentication timeout problems + + +## Required page structure + +Every documentation page must begin with YAML frontmatter: + +```yaml +--- +title: 'Clear, specific, keyword-rich title' +description: 'Concise description explaining page purpose and value' +--- +``` + +## Content quality standards + +### Code examples requirements + +- Always include complete, runnable examples that users can copy and execute +- Show proper error handling and edge case management +- Use realistic data instead of placeholder values +- Include expected outputs and results for verification +- Test all code examples thoroughly before publishing +- Specify language and include filename when relevant +- Add explanatory comments for complex logic +- Never include real API keys or secrets in code examples + +### API documentation requirements + +- Document all parameters including optional ones with clear descriptions +- Show both success and error response examples with realistic data +- Include rate limiting information with specific limits +- Provide authentication examples showing proper format +- Explain all HTTP status codes and error handling +- Cover complete request/response cycles + +### Accessibility requirements + +- Include descriptive alt text for all images and diagrams +- Use specific, actionable link text instead of "click here" +- Ensure proper heading hierarchy starting with H2 +- Provide keyboard navigation considerations +- Use sufficient color contrast in examples and visuals +- Structure content for easy scanning with headers and lists + +## Component selection logic + +- Use **Steps** for procedures and sequential instructions +- Use **Tabs** for platform-specific content or alternative approaches +- Use **CodeGroup** when showing the same concept in multiple programming languages +- Use **Accordions** for progressive disclosure of information +- Use **RequestExample/ResponseExample** specifically for API endpoint documentation +- Use **ParamField** for API parameters, **ResponseField** for API responses +- Use **Expandable** for nested object properties or hierarchical information +```` diff --git a/docs/ai-tools/windsurf.mdx b/docs/ai-tools/windsurf.mdx new file mode 100644 index 0000000..44d6882 --- /dev/null +++ b/docs/ai-tools/windsurf.mdx @@ -0,0 +1,96 @@ +--- +title: 'Windsurf setup' +description: 'Configure Windsurf for your documentation workflow' +icon: 'water' +--- + +Configure Windsurf's Cascade AI assistant to help you write and maintain documentation. This guide shows how to set up Windsurf specifically for your Mintlify documentation workflow. + +## Prerequisites + +- Windsurf editor installed +- Access to your documentation repository + +## Workspace rules + +Create workspace rules that provide Windsurf with context about your documentation project and standards. + +Create `.windsurf/rules.md` in your project root: + +````markdown +# Mintlify technical writing rule + +## Project context + +- This is a documentation project on the Mintlify platform +- We use MDX files with YAML frontmatter +- Navigation is configured in `docs.json` +- We follow technical writing best practices + +## Writing standards + +- Use second person ("you") for instructions +- Write in active voice and present tense +- Start procedures with prerequisites +- Include expected outcomes for major steps +- Use descriptive, keyword-rich headings +- Keep sentences concise but informative + +## Required page structure + +Every page must start with frontmatter: + +```yaml +--- +title: 'Clear, specific title' +description: 'Concise description for SEO and navigation' +--- +``` + +## Mintlify components + +### Callouts + +- `` for helpful supplementary information +- `` for important cautions and breaking changes +- `` for best practices and expert advice +- `` for neutral contextual information +- `` for success confirmations + +### Code examples + +- When appropriate, include complete, runnable examples +- Use `` for multiple language examples +- Specify language tags on all code blocks +- Include realistic data, not placeholders +- Use `` and `` for API docs + +### Procedures + +- Use `` component for sequential instructions +- Include verification steps with `` components when relevant +- Break complex procedures into smaller steps + +### Content organization + +- Use `` for platform-specific content +- Use `` for progressive disclosure +- Use `` and `` for highlighting content +- Wrap images in `` components with descriptive alt text + +## API documentation requirements + +- Document all parameters with `` +- Show response structure with `` +- Include both success and error examples +- Use `` for nested object properties +- Always include authentication examples + +## Quality standards + +- Test all code examples before publishing +- Use relative paths for internal links +- Include alt text for all images +- Ensure proper heading hierarchy (start with h2) +- Check existing patterns for consistency +```` diff --git a/docs/api-reference/core/errors.mdx b/docs/api-reference/core/errors.mdx new file mode 100644 index 0000000..7b947ed --- /dev/null +++ b/docs/api-reference/core/errors.mdx @@ -0,0 +1,210 @@ +--- +title: Error model +description: Typed, structured errors with a stable envelope across viem and ethers adapters. +group: API Reference / Core +--- + +## Overview + +All SDK operations either: + +1. **Throw** a `ZKsyncError` whose `.envelope` gives you a structured, stable payload, or +2. Return a **result object** from the `try*` variants: `{ ok: true, value } | { ok: false, error }`. + +This is consistent across both **ethers** and **viem** adapters. + + + Prefer the try* variants when you want to avoid exceptions and branch on + success/failure. + + +## What gets thrown + +When the SDK throws, it throws an instance of `ZKsyncError`. Use `isZKsyncError(e)` to narrow and read the **error envelope**. + +```ts +import { isZKsyncError } from '@dutterbutter/zksync-sdk/core'; + +try { + const handle = await sdk.deposits.create(params); +} catch (e) { + if (isZKsyncError(e)) { + const err = e; // type-narrowed + const { type, resource, operation, message, context, revert } = err.envelope; + + // Example: route on category + switch (type) { + case 'VALIDATION': + case 'STATE': + // user/action fixable (bad input, not-ready, etc.) + break; + case 'EXECUTION': + case 'RPC': + // network/tx/provider issues + break; + } + + // Optional: log structured data + console.error(JSON.stringify(err.toJSON())); + } else { + // Non-SDK error (framework, userland) + throw e; + } +} +``` + +## Envelope shape + + + Instance type for all SDK-thrown errors. + + +### `ZKsyncError.envelope: ErrorEnvelope` + +```ts +type ErrorEnvelope = { + /** Resource surface that raised the error. */ + resource: 'deposits' | 'withdrawals' | 'withdrawal-finalization' | 'helpers' | 'zksrpc'; + + /** Specific operation, e.g. "withdrawals.finalize" or "deposits.create". */ + operation: string; + + /** Broad category (see table below). */ + type: 'VALIDATION' | 'STATE' | 'EXECUTION' | 'RPC' | 'INTERNAL' | 'VERIFICATION' | 'CONTRACT'; + + /** Stable, human-readable message for developers. */ + message: string; + + /** Optional contextual fields (tx hash, nonce, step key, etc.). */ + context?: Record; + + /** If the error is a contract revert, adapters include decoded info when available. */ + revert?: { + selector: `0x${string}`; // 4-byte selector + name?: string; // Decoded Solidity error name + args?: unknown[]; // Decoded args + contract?: string; // Best-effort contract label + fn?: string; // Best-effort function label + }; + + /** Originating error (provider/transport/etc.), sanitized for safe logging. */ + cause?: unknown; +}; +``` + +### Categories (when to expect them) + +| Type | Meaning (how you should react) | +| :------------- | :----------------------------------------------------------------------------------------- | +| `VALIDATION` | Inputs are invalid (fix parameters and retry). | +| `STATE` | Operation not possible **yet** (e.g., not finalizable). Wait or change state. | +| `EXECUTION` | A send/revert happened (tx reverted or couldn’t be confirmed). Inspect `revert` / `cause`. | +| `RPC` | Provider/transport failure (endpoint/network issue). Retry with backoff / check infra. | +| `VERIFICATION` | Proof/verification step issue. Usually indicates unable to find deposit log. | +| `CONTRACT` | A contract read/encode/allowance failed. Check addresses & ABI compatibility. | +| `INTERNAL` | SDK internal error (please report with `operation` + `selector` if present). | + +## Result style (`try*`) helpers + +Every resource method has a `try*` sibling that never throws and returns a `TryResult`. + +```ts +const res = await sdk.withdrawals.tryCreate(params); +if (!res.ok) { + // res.error is a ZKsyncError + console.warn(res.error.envelope.message, res.error.envelope.operation); +} else { + // res.value is the success payload + console.log('l2TxHash', res.value.l2TxHash); +} +``` + +This is especially handy for UI flows where you want to surface inline validation/state messages without a `try/catch`. + +## Revert details (when transactions fail) + +If the provider exposes revert data, the adapters will decode common error types and ABIs so you can branch on them: + +```ts +try { + await sdk.withdrawals.finalize(l2TxHash); +} catch (e) { + if (isZKsyncError(e) && e.envelope.revert) { + const { selector, name, args } = e.envelope.revert; + // e.g., name === 'InvalidProof' or 'TransferAmountExceedsBalance' + } +} +``` + +Notes: + +- The SDK always includes the **4-byte selector**. +- `name`/`args` appear when decodable against known ABIs; coverage will expand over time. +- When a revert implies “not ready yet,” you’ll typically see a `STATE` error with a clarifying `message`. + +## Ethers & viem examples + + + +```ts title="Ethers" +import { JsonRpcProvider, Wallet } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +import { isZKsyncError } from '@dutterbutter/zksync-sdk/core'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); + +const res = await sdk.deposits.tryCreate({ token, amount, to }); +if (!res.ok) { +// report envelope +console.error(res.error.envelope); +} + +```` + +```ts title="Viem" +import { createPublicClient, http, createWalletClient, privateKeyToAccount } from 'viem'; +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; +import { isZKsyncError } from '@dutterbutter/zksync-sdk/core'; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`); +const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); +const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); +const l1Wallet: WalletClient = createWalletClient({ + account, + transport: http(ETH_RPC), +}); +const l2Wallet = createWalletClient({ + account, + transport: http(ZKSYNC_RPC), +}); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); + +try { + await sdk.withdrawals.finalize(l2TxHash); +} catch (e) { + if (isZKsyncError(e)) { + console.log(e.envelope.message, e.envelope.operation); + } else { + throw e; + } +} +```` + + + +## Logging & observability + +- `err.toJSON()` returns a safe, structured object you can ship to logs/telemetry. +- For local debugging, printing `err` shows a compact, human-readable view (category, operation, context, optional revert/cause). + + + Avoid parsing err.message for logic. Use the typed fields on{' '} + err.envelope instead. + diff --git a/docs/api-reference/core/rpc.mdx b/docs/api-reference/core/rpc.mdx new file mode 100644 index 0000000..9b43017 --- /dev/null +++ b/docs/api-reference/core/rpc.mdx @@ -0,0 +1,120 @@ +--- +title: zks_ RPC +description: Public ZKsync zks_ RPC methods exposed on the adapters via client.zks (Bridgehub address, L2→L1 log proofs, receipts with l2ToL1Logs). +group: API Reference / Core +--- + +### Standard Ethereum RPC (`eth_*`) + +Use your base library for all `eth_*` methods. +The client.zks +surface only covers ZKsync-specific RPC (zks\_\*). +For standard Ethereum JSON-RPC (e.g., eth_call, eth_getLogs, eth_getBalance), +call them through your chosen library (`ethers` or `viem`). + +## zks\_ Interface + +```ts +interface ZksRpc { + getBridgehubAddress(): Promise
; + getL2ToL1LogProof(txHash: Hex, index: number): Promise; + getReceiptWithL2ToL1(txHash: Hex): Promise; +} +``` + +## Methods + +### `getBridgehubAddress() → Promise
` + +Fetch the on-chain **Bridgehub** contract address. + +```ts +const addr = await client.zks.getBridgehubAddress(); +``` + +### `getL2ToL1LogProof(txHash: Hex, index: number) → Promise` + +Return a normalized proof for the **L2→L1 log** at `index` in `txHash`. + + + L2 transaction hash that emitted one or more L2→L1 logs. + + + Zero-based index of the target L2→L1 log within the transaction. + + +```ts +const proof = await client.zks.getL2ToL1LogProof(l2TxHash, 0); +/* +{ + id: bigint, + batchNumber: bigint, + proof: Hex[] +} +*/ +``` + + + If a proof isn’t available yet, this method throws a typed STATE error. Poll based on + your app’s cadence. + + +### `getReceiptWithL2ToL1(txHash: Hex) → Promise` + +Fetch the transaction receipt; the returned object **always** includes `l2ToL1Logs` (empty array if none). + +```ts +const rcpt = await client.zks.getReceiptWithL2ToL1(l2TxHash); +console.log(rcpt?.l2ToL1Logs); // always an array +``` + +## Types (overview) + +```ts +type ProofNormalized = { + id: bigint; + batchNumber: bigint; + proof: Hex[]; +}; + +type ReceiptWithL2ToL1 = { + // …standard receipt fields… + l2ToL1Logs: unknown[]; +}; +``` + +## Usage + + + +```ts title="ethers" +import { JsonRpcProvider, Wallet } from 'ethers'; +import { createEthersClient } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); + +// Public RPC surface: +const bridgehub = await client.zks.getBridgehubAddress(); +``` + +```ts title="viem" +import { createPublicClient, http } from "viem"; +import { createViemClient } from "@dutterbutter/zksync-sdk/viem"; + +const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); +const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); + +// Provide a WalletClient with an account for L1 operations. +const l1Wallet = /* your WalletClient w/ account */; + +const client = createViemClient({ l1, l2, l1Wallet }); + +// Public RPC surface: +const bridgehub = await client.zks.getBridgehubAddress(); +``` + + diff --git a/docs/api-reference/ethers/client.mdx b/docs/api-reference/ethers/client.mdx new file mode 100644 index 0000000..8f99823 --- /dev/null +++ b/docs/api-reference/ethers/client.mdx @@ -0,0 +1,141 @@ +--- +title: EthersClient +description: Low-level client for the Ethers adapter. Carries providers/signer, resolves core contract addresses, and exposes connected ethers.Contract instances. +group: SDK Reference / Ethers +--- + +## At a glance + +- **Factory:** `createEthersClient({ l1, l2, signer, overrides? }) → EthersClient` +- **What it provides:** cached core **addresses**, connected **contracts**, L2-bound **ZKsync RPC** (`zks`), and a signer force-bound to **L1**. +- **When to use:** create this first; then pass into `createEthersSdk(client)`. + +## Import + +```ts +import { createEthersClient } from '@dutterbutter/zksync-sdk/ethers'; +``` + +## Quick start + +```ts +import { JsonRpcProvider, Wallet } from 'ethers'; +import { createEthersClient } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); + +// Resolve core addresses (cached) +const addrs = await client.ensureAddresses(); + +// Connected contracts +const { bridgehub, l1AssetRouter } = await client.contracts(); +``` + + + The signer is force-bound to the **L1** provider to make L1 finalization flows work out of the + box. + + +## `createEthersClient(args) → EthersClient` + + + L1 provider for reads and L1 transactions. + + + L2 (ZKsync) provider for reads and ZK RPC. + + + Signer for sends. If not connected to args.l1, it will be connected. + + + Optional address overrides (forks/tests). + + +**Returns:** `EthersClient` + +## EthersClient interface + + + Adapter discriminator. + + + Public L1 provider. + + + Public L2 (ZKsync) provider. + + + Signer (bound to l1 for sends). + + + ZKsync-specific RPC surface bound to l2. + + +## Methods + +### `ensureAddresses() → Promise` + +Resolve and cache core contract addresses from chain state (merges any `overrides`). + +```ts +const a = await client.ensureAddresses(); +/* +{ + bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault, + l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem +} +*/ +``` + +### `contracts() → Promise<{ ...contracts }>` + +Return connected `ethers.Contract` instances for all core contracts. + +```ts +const c = await client.contracts(); +const bh = c.bridgehub; // call bh.getAddress(), bh.interface, bh.functions.*, etc. +``` + +### `refresh(): void` + +Clear cached addresses/contracts. Subsequent calls re-resolve. + +```ts +client.refresh(); +await client.ensureAddresses(); +``` + +### `baseToken(chainId: bigint) → Promise
` + +Return the **L1 base-token address** for a given L2 chain via `Bridgehub.baseToken(chainId)`. + +```ts +const base = await client.baseToken(324n /* e.g., Era */); +``` + +## Types + +### `ResolvedAddresses` + +```ts +type ResolvedAddresses = { + bridgehub: Address; + l1AssetRouter: Address; + l1Nullifier: Address; + l1NativeTokenVault: Address; + l2AssetRouter: Address; + l2NativeTokenVault: Address; + l2BaseTokenSystem: Address; +}; +``` + +## Notes & pitfalls + +- **Provider roles:** `l1` is used for L1 lookups and finalization sends; `l2` is used for ZKsync reads/RPC via `zks`. +- **Signer binding:** The signer is connected to `l1` to ensure L1 transactions (e.g., finalize) succeed without extra wiring. +- **Caching:** `ensureAddresses()` and `contracts()` are cached. Call `refresh()` after network changes or when using new overrides. +- **Overrides:** For forks or custom deployments, pass `overrides` at construction; they are merged with on-chain resolution. diff --git a/docs/api-reference/ethers/deposits.mdx b/docs/api-reference/ethers/deposits.mdx new file mode 100644 index 0000000..460cf16 --- /dev/null +++ b/docs/api-reference/ethers/deposits.mdx @@ -0,0 +1,261 @@ +--- +title: Deposits +description: L1 → L2 deposits for ETH and ERC-20 with quote, prepare, create, status, and wait helpers. +group: SDK Reference / Ethers +--- + +## At a glance + +- **Resource:** `sdk.deposits` +- **Most common flow:** `quote → create → wait({ for: 'l2' })` +- **Auto-routing:** ETH vs ERC-20 and base-token vs non-base handled internally +- **Error style:** Throwing methods (`quote`, `prepare`, `create`, `wait`) + result variants (`tryQuote`, `tryPrepare`, `tryCreate`, `tryWait`) + +## Import + +```ts +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// sdk.deposits → DepositsResource +``` + +## Quick start + +Deposit **0.1 ETH** from L1 → L2 and wait for **L2 execution**: + +```ts +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, // 0x…00 for ETH + amount: parseEther('0.1'), + to: await signer.getAddress(), +}); + +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); // null only if no L1 hash +``` + + + For UX that never throws, use the try* variants and branch on ok. + + +## Route selection (automatic) + +- `eth-base` — ETH when L2 base token **is ETH** +- `eth-nonbase` — ETH when L2 base token **≠ ETH** +- `erc20-base` — ERC-20 that **is** the L2 base token +- `erc20-nonbase` — ERC-20 that **is not** the L2 base token + +You **do not** pass a route; it’s derived from network metadata + `token`. + +## Method reference + +### `quote(p: DepositParams) → Promise` + +Estimate the operation (route, approvals, gas hints). Does **not** send txs. + + + L1 token (use 0x…00 for ETH). + + + Amount in wei. + + + L2 recipient. + + +**Returns:** `DepositQuote` + +```ts +const q = await sdk.deposits.quote({ + token: ETH_L1, + amount: parseEther('0.25'), + to: await signer.getAddress(), +}); +/* +{ + route: "eth-base" | "eth-nonbase" | "erc20-base" | "erc20-nonbase", + approvalsNeeded: [{ token, spender, amount }], + baseCost?: bigint, + mintValue?: bigint, + suggestedL2GasLimit?: bigint, + gasPerPubdata?: bigint +} +*/ +``` + + + If approvalsNeeded is non-empty (ERC-20), create will include those + steps automatically. + + +### `tryQuote(p) → Promise<{ ok: true; value: DepositQuote } | { ok: false; error }>` + +Result-style `quote`. + +### `prepare(p: DepositParams) → Promise>` + +Builds the plan (ordered steps + unsigned txs) without sending. + +**Returns:** `DepositPlan` + +```ts +const plan = await sdk.deposits.prepare({ token: ETH_L1, amount: parseEther('0.05'), to }); +/* +{ + route, + summary: DepositQuote, + steps: [ + { key: "approve:USDC", kind: "approve", tx: TransactionRequest }, + { key: "bridge", kind: "bridge", tx: TransactionRequest } + ] +} +*/ +``` + +### `tryPrepare(p) → Promise<{ ok: true; value: DepositPlan } | { ok: false; error }>` + +Result-style `prepare`. + +### `create(p: DepositParams) → Promise>` + +Prepares and **executes** all required L1 steps. Returns a handle (with L1 tx hash and per-step hashes). + +**Returns:** `DepositHandle` + +```ts +const handle = await sdk.deposits.create({ token, amount, to }); +/* +{ + kind: "deposit", + l1TxHash: Hex, + stepHashes: Record, + plan: DepositPlan +} +*/ +``` + + + If any step reverts, create throws a typed error. Prefer tryCreate to + avoid exceptions. + + +### `tryCreate(p) → Promise<{ ok: true; value: DepositHandle } | { ok: false; error }>` + +Result-style `create`. + +### `status(handleOrHash) → Promise` + +Resolve current phase for a deposit. Accepts the `DepositHandle` from `create` **or** a raw L1 tx hash. + +**Phases** + +- `UNKNOWN` — no L1 hash provided +- `L1_PENDING` — L1 receipt not yet found +- `L1_INCLUDED` — included on L1; L2 hash not derivable yet +- `L2_PENDING` — L2 hash known; waiting for L2 receipt +- `L2_EXECUTED` — L2 receipt found with `status === 1` +- `L2_FAILED` — L2 receipt found with `status !== 1` + +```ts +const s = await sdk.deposits.status(handle); +// { phase, l1TxHash, l2TxHash? } +``` + +### `wait(handleOrHash, { for: 'l1' | 'l2' }) → Promise` + +Block until the chosen checkpoint. + +- `{ for: 'l1' }` → L1 receipt (or `null` if no L1 hash available) +- `{ for: 'l2' }` → L2 receipt after canonical execution (or `null` if no L1 hash) + +```ts +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +``` + +### `tryWait(handleOrHash, opts) → Result` + +Result-style `wait`. + +## End-to-end examples + +### ETH deposit (typical) + +```ts +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: await signer.getAddress(), +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); +``` + +### ERC-20 deposit (with automatic approvals) + +```ts +const handle = await sdk.deposits.create({ + token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // example: USDC + amount: 1_000_000n, // 1.0 USDC (6 dp) + to: await signer.getAddress(), +}); + +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +``` + +## Types (overview) + +```ts +type DepositParams = { + token: Address; // 0x…00 for ETH + amount: bigint; // wei + to: Address; // L2 recipient +}; + +type DepositQuote = { + route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; + approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; + baseCost?: bigint; + mintValue?: bigint; + suggestedL2GasLimit?: bigint; + gasPerPubdata?: bigint; +}; + +type DepositPlan = { + route: DepositQuote['route']; + summary: DepositQuote; + steps: Array<{ key: string; kind: string; tx: TTx }>; +}; + +type DepositHandle = { + kind: 'deposit'; + l1TxHash: Hex; + stepHashes: Record; + plan: DepositPlan; +}; + +type DepositStatus = + | { phase: 'UNKNOWN'; l1TxHash: Hex } + | { phase: 'L1_PENDING'; l1TxHash: Hex } + | { phase: 'L1_INCLUDED'; l1TxHash: Hex } + | { phase: 'L2_PENDING'; l1TxHash: Hex; l2TxHash: Hex } + | { phase: 'L2_EXECUTED'; l1TxHash: Hex; l2TxHash: Hex } + | { phase: 'L2_FAILED'; l1TxHash: Hex; l2TxHash: Hex }; +``` + + + Prefer the try* variants if you want to avoid exceptions and work with result + objects. + + +## Notes & pitfalls + +- **ETH sentinel:** use the canonical `0x…00` address when passing ETH as `token`. +- **Receipts timing:** `wait({ for: 'l2' })` resolves on canonical L2 execution; it can take longer than L1 inclusion. +- **Gas hints:** `suggestedL2GasLimit` and `gasPerPubdata` are hints; advanced users may override via low-level calls from the plan. diff --git a/docs/api-reference/ethers/sdk.mdx b/docs/api-reference/ethers/sdk.mdx new file mode 100644 index 0000000..5202cb2 --- /dev/null +++ b/docs/api-reference/ethers/sdk.mdx @@ -0,0 +1,138 @@ +--- +title: EthersSdk +description: High-level SDK composed over the Ethers adapter - deposits, withdrawals, and chain-aware helpers. +group: SDK Reference / Ethers +--- + +## At a glance + +- **Factory:** `createEthersSdk(client) → EthersSdk` +- **Composed resources:** `sdk.deposits`, `sdk.withdrawals`, `sdk.helpers` +- **Client vs SDK:** the **client** wires RPC/signing; the **sdk** adds high-level flows (quote → prepare → create → wait) and convenience helpers. + +## Import + +```ts +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +``` + +## Quick start + +```ts +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); + +// Example: deposit 0.05 ETH L1 → L2, wait for L2 execution +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, // 0x…00 sentinel for ETH supported + amount: parseEther('0.05'), + to: await signer.getAddress(), +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); + +// Example: resolve core contracts +const { l1NativeTokenVault } = await sdk.helpers.contracts(); +``` + +## `createEthersSdk(client) → EthersSdk` + + + Instance returned by `createEthersClient({(l1, l2, signer)})`. + + +**Returns:** `EthersSdk` + + + The SDK composes the client with resources: deposits, withdrawals, and + convenience helpers. + + +## EthersSdk interface + +### `deposits: DepositsResource` + +L1 → L2 flows. See **[Deposits](/api-reference/ethers/deposits)**. + +### `withdrawals: WithdrawalsResource` + +L2 → L1 flows. See **[Withdrawals](/api-reference/ethers/withdrawals)**. + +## helpers + +Utilities for chain addresses, connected contracts, and L1↔L2 token mapping. + +### `addresses() → Promise` + +Resolve core addresses (Bridgehub, routers, vaults, base-token system). + +```ts +const a = await sdk.helpers.addresses(); +``` + +### `contracts() → Promise<{ ...contracts }>` + +Connected `ethers.Contract` instances for all core contracts. + +```ts +const c = await sdk.helpers.contracts(); +``` + +### One-off contract getters + +`l1AssetRouter() → Promise` +`l1NativeTokenVault() → Promise` +`l1Nullifier() → Promise` + +```ts +const nullifier = await sdk.helpers.l1Nullifier(); +``` + +### `baseToken(chainId?: bigint) → Promise
` + +L1 address of the **base token** for the current (or supplied) L2 chain. + +```ts +const base = await sdk.helpers.baseToken(); // infers from client.l2 +``` + +### `l2TokenAddress(l1Token: Address) → Promise
` + +L2 token address for an L1 token. + +- Handles ETH special case (L2 ETH placeholder). +- If token is the chain’s base token, returns the L2 base-token system address. +- Otherwise queries `IL2NativeTokenVault.l2TokenAddress`. + +```ts +const l2Crown = await sdk.helpers.l2TokenAddress(CROWN_ERC20_ADDRESS); +``` + +### `l1TokenAddress(l2Token: Address) → Promise
` + +L1 token for an L2 token via `IL2AssetRouter.l1TokenAddress`. ETH placeholder resolves to canonical ETH. + +```ts +const l1Crown = await sdk.helpers.l1TokenAddress(L2_CROWN_ADDRESS); +``` + +### `assetId(l1Token: Address) → Promise` + +`bytes32` asset ID via `L1NativeTokenVault.assetId` (ETH handled canonically). + +```ts +const id = await sdk.helpers.assetId(CROWN_ERC20_ADDRESS); +``` + +## Notes & pitfalls + +- **Client first:** You must construct the **client** with `{ l1, l2, signer }` before creating the SDK. +- **Chain-derived behavior:** helpers pull from on-chain sources; results depend on the connected networks. +- **Error model:** resource methods throw typed errors; prefer try\* variants on resources for result objects. diff --git a/docs/api-reference/ethers/withdrawals.mdx b/docs/api-reference/ethers/withdrawals.mdx new file mode 100644 index 0000000..80416a4 --- /dev/null +++ b/docs/api-reference/ethers/withdrawals.mdx @@ -0,0 +1,265 @@ +--- +title: Withdrawals +description: L2 → L1 withdrawals for ETH and ERC-20 with quote, prepare, create, status, wait, and finalize helpers. +group: SDK Reference / Ethers +--- + +## At a glance + +- **Resource:** `sdk.withdrawals` +- **Typical flow:** `quote → create → wait({ for: 'l2' }) → wait({ for: 'ready' }) → finalize` +- **Auto-routing:** ETH vs ERC-20 and base-token vs non-base handled internally +- **Error style:** Throwing methods (`quote`, `prepare`, `create`, `status`, `wait`, `finalize`) + result variants (`tryQuote`, `tryPrepare`, `tryCreate`, `tryWait`, `tryFinalize`) + +## Import + +```ts +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// sdk.withdrawals → WithdrawalsResource +``` + +## Quick start + +Withdraw **0.1 ETH** from L2 → L1 and finalize on L1: + +```ts +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: await signer.getAddress(), // L1 recipient +}); + +// 1) L2 inclusion (adds l2ToL1Logs if available) +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// 2) Wait until finalizable (no side effects) +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000 }); + +// 3) Finalize on L1 (no-op if already finalized) +const { status, receipt: l1Receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +``` + + +Withdrawals are two-phase: inclusion on **L2**, then **finalization on L1**. You can call finalize directly; it will throw if not yet ready. Prefer `wait(..., { for: 'ready' })` to avoid that. + + +## Route selection (automatic) + +- `eth-base` — Base token is **ETH** on L2 +- `eth-nonbase` — Base token is **not ETH** on L2 +- `erc20-nonbase` — Withdrawing an ERC-20 that is **not** the base token + +You **do not** pass a route; it’s derived from network metadata + `token`. + +## Method reference + +### `quote(p: WithdrawParams) → Promise` + +Estimate the operation (route, approvals, gas hints). Does **not** send txs. + + + L2 token (ETH handled by the SDK; ETH sentinel supported). + + + Amount in wei. + + + L1 recipient. + + +**Returns:** `WithdrawQuote` + +```ts +const q = await sdk.withdrawals.quote({ token, amount, to }); +/* +{ + route: "eth-base" | "eth-nonbase" | "erc20-nonbase", + approvalsNeeded: [{ token, spender, amount }], + suggestedL2GasLimit?: bigint +} +*/ +``` + +### `tryQuote(p) → Promise<{ ok: true; value: WithdrawQuote } | { ok: false; error }>` + +Result-style `quote`. + +### `prepare(p: WithdrawParams) → Promise>` + +Builds the plan (ordered L2 steps + unsigned txs) without sending. + +**Returns:** `WithdrawPlan` + +```ts +const plan = await sdk.withdrawals.prepare({ token, amount, to }); +/* +{ + route, + summary: WithdrawQuote, + steps: [ + { key, kind, tx: TransactionRequest }, + // … + ] +} +*/ +``` + +### `tryPrepare(p) → Promise<{ ok: true; value: WithdrawPlan } | { ok: false; error }>` + +Result-style `prepare`. + +### `create(p: WithdrawParams) → Promise>` + +Prepares and **executes** required **L2** steps. Returns a handle with the **L2 tx hash**. + +**Returns:** `WithdrawHandle` + +```ts +const handle = await sdk.withdrawals.create({ token, amount, to }); +/* +{ + kind: "withdrawal", + l2TxHash: Hex, + stepHashes: Record, + plan: WithdrawPlan +} +*/ +``` + + + If any L2 step reverts, create throws a typed error. Prefer tryCreate{' '} + for a result object. + + +### `tryCreate(p) → Promise<{ ok: true; value: WithdrawHandle } | { ok: false; error }>` + +Result-style `create`. + +### `status(handleOrHash) → Promise` + +Report current phase for a withdrawal. Accepts the `WithdrawHandle` from `create` **or** a raw **L2 tx hash**. + +**Phases** + +- `UNKNOWN` — no L2 hash provided +- `L2_PENDING` — L2 receipt missing +- `PENDING` — included on L2 but not yet finalizable +- `READY_TO_FINALIZE` — can be finalized on L1 now +- `FINALIZED` — already finalized on L1 + +```ts +const s = await sdk.withdrawals.status(handle); +// { phase, l2TxHash, key? } +``` + +### `wait(handleOrHash, { for: 'l2' | 'ready' | 'finalized', pollMs?, timeoutMs? })` + +Block until a target is reached. + +- `{ for: 'l2' }` → resolves **L2 receipt** (`TransactionReceiptZKsyncOS`) or `null` +- `{ for: 'ready' }` → resolves `null` when finalizable +- `{ for: 'finalized' }` → resolves **L1 receipt** (if found) or `null` + +```ts +const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000, timeoutMs: 15 * 60_000 }); +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', pollMs: 7000 }); +``` + + + Default polling is 5500 ms (min 1000 ms). Use timeoutMs for long windows. + + +### `tryWait(handleOrHash, opts) → Result` + +Result-style `wait`. + +### `finalize(l2TxHash: Hex) → Promise<{ status: WithdrawalStatus; receipt?: TransactionReceipt }>` + +Sends the **L1 finalize** transaction **if** ready. If already finalized, returns the status without sending. + +```ts +const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +if (status.phase === 'FINALIZED') { + console.log('L1 tx:', receipt?.transactionHash); +} +``` + + +If not ready, finalize throws a typed STATE error. Use status(...) or `wait(..., { for: 'ready' })` first to avoid throws. + + +### `tryFinalize(l2TxHash) → Promise<{ ok: true; value: { status: WithdrawalStatus; receipt?: TransactionReceipt } } | { ok: false; error }>` + +Result-style `finalize`. + +## End-to-end examples + +### Minimal happy path + +```ts +const handle = await sdk.withdrawals.create({ token, amount, to }); + +// L2 inclusion +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// Option A: finalize immediately (will throw if not ready) +await sdk.withdrawals.finalize(handle.l2TxHash); + +// Option B: wait for readiness, then finalize +await sdk.withdrawals.wait(handle, { for: 'ready' }); +await sdk.withdrawals.finalize(handle.l2TxHash); +``` + +## Types (overview) + +```ts +type WithdrawParams = { + token: Address; // L2 token (ETH sentinel supported) + amount: bigint; // wei + to: Address; // L1 recipient +}; + +type WithdrawQuote = { + route: 'eth-base' | 'eth-nonbase' | 'erc20-nonbase'; + approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; + suggestedL2GasLimit?: bigint; +}; + +type WithdrawPlan = { + route: WithdrawQuote['route']; + summary: WithdrawQuote; + steps: Array<{ key: string; kind: string; tx: TTx }>; +}; + +type WithdrawHandle = { + kind: 'withdrawal'; + l2TxHash: Hex; + stepHashes: Record; + plan: WithdrawPlan; +}; + +type WithdrawalStatus = + | { phase: 'UNKNOWN'; l2TxHash: Hex } + | { phase: 'L2_PENDING'; l2TxHash: Hex } + | { phase: 'PENDING'; l2TxHash: Hex; key?: unknown } + | { phase: 'READY_TO_FINALIZE'; l2TxHash: Hex; key: unknown } + | { phase: 'FINALIZED'; l2TxHash: Hex; key: unknown }; + +// L2 receipt augmentation returned by wait({ for: 'l2' }) +type TransactionReceiptZKsyncOS = TransactionReceipt & { l2ToL1Logs?: Array }; +``` + +## Notes & pitfalls + +- **Two chains, two receipts:** inclusion on **L2** and finalization on **L1** are independent events. +- **Polling strategy:** for production UIs, prefer `wait({ for: 'ready' })` then finalize; it avoids premature finalize calls. +- **Approvals:** if withdrawing ERC-20 requires approvals, `create` will include those steps automatically. diff --git a/docs/api-reference/introduction.mdx b/docs/api-reference/introduction.mdx new file mode 100644 index 0000000..55495ab --- /dev/null +++ b/docs/api-reference/introduction.mdx @@ -0,0 +1,127 @@ +--- +title: Introduction +description: Public, typed API surface for the ZKsyncOS SDK — Incorruptible Financial Infrastructure. +group: API Reference / Overview +--- + +## What is this? + +The **ZKsyncOS SDK** provides lightweight adapters for **ethers** and **viem** to build L1 ↔ L2 flows—**deposits** and **withdrawals**—with a small, focused API. You’ll work with: + +- Adapter-level **Clients** (providers/wallets, resolved addresses, convenience contracts) +- High-level **SDKs** (resources for deposits & withdrawals + helpers) +- ZKsync-specific **RPC** helpers (`client.zks.*`) +- A consistent, typed **Error model** (`ZKsyncError`, `try*` results) + +## Quick start + + + + +```ts +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk, ETH_ADDRESS } from '@dutterbutter/zksync-sdk/ethers'; + +const l1 = new JsonRpcProvider(process.env.ETH_RPC!); +const l2 = new JsonRpcProvider(process.env.ZKSYNC_RPC!); +const signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +// Low-level client + high-level SDK +const client = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); + +// Deposit 0.05 ETH L1 → L2 and wait for L2 execution +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.05'), + to: await signer.getAddress(), +}); + +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); + +// ZKsync-specific RPC is available via client.zks +const bridgehub = await client.zks.getBridgehubAddress(); +``` + + + + +```ts +import { + createPublicClient, + http, + createWalletClient, + privateKeyToAccount, + parseEther, +} from 'viem'; +import { createViemClient, createViemSdk, ETH_ADDRESS } from '@dutterbutter/zksync-sdk/viem'; + +const account = privateKeyToAccount(process.env.PRIVATE_KEY! as `0x${string}`); +const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); +const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); +const l1Wallet = createWalletClient({ account, transport: http(process.env.ETH_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet }); +const sdk = createViemSdk(client); + +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, + amount: parseEther('0.05'), + to: account.address, // L1 recipient +}); + +await sdk.withdrawals.wait(handle, { for: 'l2' }); // inclusion on L2 +const { status } = await sdk.withdrawals.finalize(handle.l2TxHash); // finalize on L1 + +const bridgehub = await client.zks.getBridgehubAddress(); +``` + + + + +## What’s documented here + + + + Low-level handle: providers/signer, resolved addresses, convenience contracts, ZK RPC access. + + + High-level deposits/withdrawals plus helpers for addresses, contracts, and token mapping. + + + L1 → L2 flow with quote, prepare, create, status, and wait. + + + L2 → L1 flow with quote, prepare, create, status, wait, and finalize. + + + + PublicClient/WalletClient integration, resolved addresses, contracts, ZK RPC access. + + + Same high-level surface as ethers, typed to viem contracts. + + + L1 → L2 flow with quote, prepare, create, status, and wait. + + + L2 → L1 flow with quote, prepare, create, status, wait, and finalize. + + + + ZKsync-specific RPC: getBridgehubAddress, getL2ToL1LogProof, enhanced receipts. + + + Typed ZKsyncError envelope and try* result helpers. + + + +## Notes & conventions + +- **Standard eth\_\* RPC** should be performed through your chosen base library (**ethers** / **viem**). + The SDK only adds ZKsync-specific RPC via client.zks.\* (e.g., getBridgehubAddress, getL2ToL1LogProof, enhanced receipts). + +- Every resource method has a try\* variant (e.g., tryCreate) that returns a result object instead of throwing. + When errors occur, the SDK throws ZKsyncError with a stable envelope (see Error model). + +- Address resolution comes from on-chain lookups and well-known constants. You can override addresses in the client constructor for forks/tests. diff --git a/docs/api-reference/viem/client.mdx b/docs/api-reference/viem/client.mdx new file mode 100644 index 0000000..8b809ba --- /dev/null +++ b/docs/api-reference/viem/client.mdx @@ -0,0 +1,169 @@ +--- +title: ViemClient +description: Low-level client for the Viem adapter. Carries viem public/wallet clients, resolves core contract addresses, and exposes typed contract instances. +group: SDK Reference / Viem +--- + +## At a glance + +- **Factory:** `createViemClient({ l1, l2, l1Wallet, l2Wallet?, overrides? }) → ViemClient` +- **What it provides:** cached core **addresses**, typed **contracts**, convenience **wallet access** (L2 wallet derivation), and ZKsync **RPC** bound to `l2`. +- **When to use:** create this first; then pass into `createViemSdk(client)`. + +## Import + +```ts +import { createViemClient } from '@dutterbutter/zksync-sdk/viem'; +``` + +## Quick start + +```ts +import { createPublicClient, createWalletClient, http } from "viem"; + +// Public clients (reads) +const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); +const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); + +// Wallet clients (writes) +const l1Wallet = createWalletClient({ + account: /* your L1 Account */, + transport: http(process.env.ETH_RPC!), +}); + +// Optional dedicated L2 wallet. Required for L2 sends (withdrawals). +const l2Wallet = createWalletClient({ + account: /* can be same key as L1 */, + transport: http(process.env.ZKSYNC_RPC!), +}); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); + +// Resolve core addresses (cached) +const addrs = await client.ensureAddresses(); + +// Typed contracts (viem getContract) +const { bridgehub, l1AssetRouter } = await client.contracts(); +``` + + + l1Wallet.account is required. If you omit l2Wallet, use{' '} + client.getL2Wallet(); it lazily reuses the L1 account over the L2 transport. + + +## `createViemClient(args) → ViemClient` + + + L1 client for reads and chain metadata. + + + L2 (ZKsync) client for reads and ZK RPC. + + + L1 wallet (must include account) used for L1 sends. + + + Optional L2 wallet for L2 sends; otherwise derived via getL2Wallet(). + + + Optional contract-address overrides (forks/tests). + + +**Returns:** `ViemClient` + +## ViemClient interface + + + Adapter discriminator. + + + Public L1 client. + + + Public L2 (ZKsync) client. + + + Wallet bound to L1 (carries default account). + + + Optional pre-supplied L2 wallet. + + + Default account (from l1Wallet). + + + ZKsync-specific RPC bound to l2. + + +## Methods + +### `ensureAddresses() → Promise` + +Resolve and cache core contract addresses from chain state (merges any `overrides`). + +```ts +const a = await client.ensureAddresses(); +/* +{ + bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault, + l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem +} +*/ +``` + +### `contracts() → Promise<{ ...contracts }>` + +Return **typed** viem contracts (`getContract`) connected to the current clients. + +```ts +const c = await client.contracts(); +const bh = c.bridgehub; // bh.read.*, bh.write.*, bh.simulate.* +``` + +### `refresh(): void` + +Clear cached addresses/contracts. Subsequent calls re-resolve. + +```ts +client.refresh(); +await client.ensureAddresses(); +``` + +### `baseToken(chainId: bigint) → Promise
` + +Return the **L1 base-token address** for a given L2 chain via `Bridgehub.baseToken(chainId)`. + +```ts +const base = await client.baseToken(324n /* example L2 chain id */); +``` + +### `getL2Wallet() → viem.WalletClient` + +Return the L2 wallet. If not provided at construction, lazily creates one from the **same account** as `l1Wallet` over the L2 transport. + +```ts +const w = client.getL2Wallet(); // ensures L2 writes are possible +``` + +## Types + +### `ResolvedAddresses` + +```ts +type ResolvedAddresses = { + bridgehub: Address; + l1AssetRouter: Address; + l1Nullifier: Address; + l1NativeTokenVault: Address; + l2AssetRouter: Address; + l2NativeTokenVault: Address; + l2BaseTokenSystem: Address; +}; +``` + +## Notes & pitfalls + +- **Wallet placement:** Deposits sign on **L1**; withdrawals sign on **L2**; finalization signs on **L1** (via SDK). +- **Caching:** `ensureAddresses()` and `contracts()` are cached. Use `refresh()` after network/override changes. +- **Overrides:** For forks or custom deployments, pass `overrides` at construction; they’ll be merged with on-chain lookups. +- **Error surface:** Methods may throw typed errors; use the SDK’s `try*` variants (on resources) if you prefer result objects. diff --git a/docs/api-reference/viem/deposits.mdx b/docs/api-reference/viem/deposits.mdx new file mode 100644 index 0000000..65b0897 --- /dev/null +++ b/docs/api-reference/viem/deposits.mdx @@ -0,0 +1,276 @@ +--- +title: Deposits +description: L1 → L2 deposits for ETH and ERC-20 with quote, prepare, create, status, and wait helpers (Viem adapter). +group: API Reference / Viem +--- + +## At a glance + +- **Resource:** `sdk.deposits` +- **Most common flow:** `quote → create → wait({ for: 'l2' })` +- **Auto-routing:** ETH vs ERC-20 and base-token vs non-base handled internally +- **Error style:** Throwing methods (`quote`, `prepare`, `create`, `wait`) + result variants (`tryQuote`, `tryPrepare`, `tryCreate`, `tryWait`) + +## Import + +```ts +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; + +const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); +const l1 = createPublicClient({ transport: http(L1_RPC) }); +const l2 = createPublicClient({ transport: http(L2_RPC) }); +const l1Wallet: WalletClient = createWalletClient({ + account, + transport: http(L1_RPC), +}); + +// --- 2. Initialize the SDK --- +const client = createViemClient({ l1, l2, l1Wallet }); +const sdk = createViemSdk(client); +// sdk.deposits → DepositsResource +``` + +## Quick start + +Deposit **0.1 ETH** from L1 → L2 and wait for **L2 execution**: + +```ts +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, // 0x…00 for ETH + amount: parseEther('0.1'), + to: await signer.getAddress(), +}); + +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); // null only if no L1 hash +``` + + + For UX that never throws, use the try* variants and branch on ok. + + +## Route selection (automatic) + +- `eth-base` — ETH when L2 base token **is ETH** +- `eth-nonbase` — ETH when L2 base token **≠ ETH** +- `erc20-base` — ERC-20 that **is** the L2 base token +- `erc20-nonbase` — ERC-20 that **is not** the L2 base token + +You **do not** pass a route; it’s derived from network metadata + `token`. + +## Method reference + +### `quote(p: DepositParams) → Promise` + +Estimate the operation (route, approvals, gas hints). Does **not** send txs. + + + L1 token (use 0x…00 for ETH). + + + Amount in wei. + + + L2 recipient. + + +**Returns:** `DepositQuote` + +```ts +const q = await sdk.deposits.quote({ + token: ETH_L1, + amount: parseEther('0.25'), + to: await signer.getAddress(), +}); +/* +{ + route: "eth-base" | "eth-nonbase" | "erc20-base" | "erc20-nonbase", + approvalsNeeded: [{ token, spender, amount }], + baseCost?: bigint, + mintValue?: bigint, + suggestedL2GasLimit?: bigint, + gasPerPubdata?: bigint +} +*/ +``` + + + If approvalsNeeded is non-empty (ERC-20), create will include those + steps automatically. + + +### `tryQuote(p) → Promise<{ ok: true; value: DepositQuote } | { ok: false; error }>` + +Result-style `quote`. + +### `prepare(p: DepositParams) → Promise>` + +Builds the plan (ordered steps + unsigned txs) without sending. + +**Returns:** `DepositPlan` + +```ts +const plan = await sdk.deposits.prepare({ token: ETH_L1, amount: parseEther('0.05'), to }); +/* +{ + route, + summary: DepositQuote, + steps: [ + { key: "approve:USDC", kind: "approve", tx: TransactionRequest }, + { key: "bridge", kind: "bridge", tx: TransactionRequest } + ] +} +*/ +``` + +### `tryPrepare(p) → Promise<{ ok: true; value: DepositPlan } | { ok: false; error }>` + +Result-style `prepare`. + +### `create(p: DepositParams) → Promise>` + +Prepares and **executes** all required L1 steps. Returns a handle (with L1 tx hash and per-step hashes). + +**Returns:** `DepositHandle` + +```ts +const handle = await sdk.deposits.create({ token, amount, to }); +/* +{ + kind: "deposit", + l1TxHash: Hex, + stepHashes: Record, + plan: DepositPlan +} +*/ +``` + + + If any step reverts, create throws a typed error. Prefer tryCreate to + avoid exceptions. + + +### `tryCreate(p) → Promise<{ ok: true; value: DepositHandle } | { ok: false; error }>` + +Result-style `create`. + +### `status(handleOrHash) → Promise` + +Resolve current phase for a deposit. Accepts the `DepositHandle` from `create` **or** a raw L1 tx hash. + +**Phases** + +- `UNKNOWN` — no L1 hash provided +- `L1_PENDING` — L1 receipt not yet found +- `L1_INCLUDED` — included on L1; L2 hash not derivable yet +- `L2_PENDING` — L2 hash known; waiting for L2 receipt +- `L2_EXECUTED` — L2 receipt found with `status === 1` +- `L2_FAILED` — L2 receipt found with `status !== 1` + +```ts +const s = await sdk.deposits.status(handle); +// { phase, l1TxHash, l2TxHash? } +``` + +### `wait(handleOrHash, { for: 'l1' | 'l2' }) → Promise` + +Block until the chosen checkpoint. + +- `{ for: 'l1' }` → L1 receipt (or `null` if no L1 hash available) +- `{ for: 'l2' }` → L2 receipt after canonical execution (or `null` if no L1 hash) + +```ts +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +``` + +### `tryWait(handleOrHash, opts) → Result` + +Result-style `wait`. + +## End-to-end examples + +### ETH deposit (typical) + +```ts +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: await signer.getAddress(), +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); +``` + +### ERC-20 deposit (with automatic approvals) + +```ts +const handle = await sdk.deposits.create({ + token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // example: USDC + amount: 1_000_000n, // 1.0 USDC (6 dp) + to: await signer.getAddress(), +}); + +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +``` + +## Types (overview) + +```ts +type DepositParams = { + token: Address; // 0x…00 for ETH + amount: bigint; // wei + to: Address; // L2 recipient +}; + +type DepositQuote = { + route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; + approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; + baseCost?: bigint; + mintValue?: bigint; + suggestedL2GasLimit?: bigint; + gasPerPubdata?: bigint; +}; + +type DepositPlan = { + route: DepositQuote['route']; + summary: DepositQuote; + steps: Array<{ key: string; kind: string; tx: TTx }>; +}; + +type DepositHandle = { + kind: 'deposit'; + l1TxHash: Hex; + stepHashes: Record; + plan: DepositPlan; +}; + +type DepositStatus = + | { phase: 'UNKNOWN'; l1TxHash: Hex } + | { phase: 'L1_PENDING'; l1TxHash: Hex } + | { phase: 'L1_INCLUDED'; l1TxHash: Hex } + | { phase: 'L2_PENDING'; l1TxHash: Hex; l2TxHash: Hex } + | { phase: 'L2_EXECUTED'; l1TxHash: Hex; l2TxHash: Hex } + | { phase: 'L2_FAILED'; l1TxHash: Hex; l2TxHash: Hex }; +``` + + + Prefer the try* variants if you want to avoid exceptions and work with result + objects. + + +## Notes & pitfalls + +- **ETH sentinel:** use the canonical `0x…00` address when passing ETH as `token`. +- **Receipts timing:** `wait({ for: 'l2' })` resolves on canonical L2 execution; it can take longer than L1 inclusion. +- **Gas hints:** `suggestedL2GasLimit` and `gasPerPubdata` are hints; advanced users may override via low-level calls from the plan. diff --git a/docs/api-reference/viem/sdk.mdx b/docs/api-reference/viem/sdk.mdx new file mode 100644 index 0000000..c4786a9 --- /dev/null +++ b/docs/api-reference/viem/sdk.mdx @@ -0,0 +1,159 @@ +--- +title: ViemSdk +description: High-level SDK composed over the Viem adapter - deposits, withdrawals, and chain-aware helpers. +group: SDK Reference / Viem +--- + +## At a glance + +- **Factory:** `createViemSdk(client) → ViemSdk` +- **Composed resources:** `sdk.deposits`, `sdk.withdrawals`, `sdk.helpers` +- **Client vs SDK:** the **client** wires RPC/signing; the **sdk** adds high-level flows (quote → prepare → create → wait) and convenience helpers. +- **Wallets by flow:** + - **Deposits (L1 tx):** `l1Wallet` required + - **Withdrawals (L2 tx):** `l2Wallet` required + - **Finalize (L1 tx):** `l1Wallet` required + +## Import + +```ts +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; +``` + +## Quick start + +```ts +import { createPublicClient, createWalletClient, http } from "viem"; +import { createViemClient, createViemSdk } from "@dutterbutter/zksync-sdk/viem"; + +// Public clients (reads) +const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); +const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); + +// Wallet clients (writes) +const l1Wallet = createWalletClient({ + account: /* your L1 Account */, + transport: http(process.env.ETH_RPC!), +}); + +const l2Wallet = createWalletClient({ + account: /* your L2 Account (can be the same key) */, + transport: http(process.env.ZKSYNC_RPC!), +}); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); + +// Example: deposit 0.05 ETH L1 → L2, wait for L2 execution +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, // 0x…00 sentinel for ETH supported + amount: 50_000_000_000_000_000n, // 0.05 ETH in wei + to: l2Wallet.account.address, +}); +await sdk.deposits.wait(handle, { for: "l2" }); + +// Example: resolve contracts and map an L1 token to its L2 address +const { l1NativeTokenVault } = await sdk.helpers.contracts(); +const l2Crown = await sdk.helpers.l2TokenAddress(CROWN_ERC20_ADDRESS); +``` + + + You can construct the client with only the wallets you need for a given flow (e.g., just{' '} + l2Wallet to create withdrawals; add l1Wallet when you plan to finalize). + + +## `createViemSdk(client) → ViemSdk` + + +Instance returned by `createViemClient({ l1, l2, l1Wallet?, l2Wallet? })`. + + +**Returns:** `ViemSdk` + + + The SDK composes the client with resources: deposits, withdrawals, and + convenience helpers. + + +## ViemSdk interface + +### `deposits: DepositsResource` + +L1 → L2 flows. See **[Deposits](/api-reference/viem/deposits)**. + +### `withdrawals: WithdrawalsResource` + +L2 → L1 flows. See **[Withdrawals](/api-reference/viem/withdrawals)**. + +## helpers + +Utilities for chain addresses, connected contracts, and L1↔L2 token mapping. + +### `addresses() → Promise` + +Resolve core addresses (Bridgehub, routers, vaults, base-token system). + +```ts +const a = await sdk.helpers.addresses(); +``` + +### `contracts() → Promise<{ ...contracts }>` + +**Typed** Viem contracts for all core components (each exposes `.read` / `.write` / `.simulate`). + +```ts +const c = await sdk.helpers.contracts(); +const bridgehub = c.bridgehub; +``` + +### One-off contract getters + +`l1AssetRouter() → Promise` +`l1NativeTokenVault() → Promise` +`l1Nullifier() → Promise` + +```ts +const nullifier = await sdk.helpers.l1Nullifier(); +``` + +### `baseToken(chainId?: bigint) → Promise
` + +L1 address of the **base token** for the current (or supplied) L2 chain. + +```ts +const base = await sdk.helpers.baseToken(); // infers from the L2 client +``` + +### `l2TokenAddress(l1Token: Address) → Promise
` + +L2 token address for an L1 token. + +- Handles ETH special case (L2 ETH placeholder). +- If the token is the chain’s base token, returns the L2 base-token system address. +- Otherwise queries `IL2NativeTokenVault.l2TokenAddress`. + +```ts +const l2Crown = await sdk.helpers.l2TokenAddress(CROWN_ERC20_ADDRESS); +``` + +### `l1TokenAddress(l2Token: Address) → Promise
` + +L1 token for an L2 token via `IL2AssetRouter.l1TokenAddress`. ETH placeholder resolves to canonical ETH. + +```ts +const l1Crown = await sdk.helpers.l1TokenAddress(L2_CROWN_ADDRESS); +``` + +### `assetId(l1Token: Address) → Promise` + +`bytes32` asset ID via `L1NativeTokenVault.assetId` (ETH handled canonically). + +```ts +const id = await sdk.helpers.assetId(CROWN_ERC20_ADDRESS); +``` + +## Notes & pitfalls + +- **Wallet placement matters:** deposits sign on **L1**; withdrawals sign on **L2**; finalization signs on **L1**. +- **Chain-derived behavior:** helpers read from on-chain sources; results depend on connected networks. +- **Error model:** resource methods throw typed errors; prefer try\* variants on resources for result objects. diff --git a/docs/api-reference/viem/withdrawals.mdx b/docs/api-reference/viem/withdrawals.mdx new file mode 100644 index 0000000..74cad4b --- /dev/null +++ b/docs/api-reference/viem/withdrawals.mdx @@ -0,0 +1,280 @@ +--- +title: Withdrawals +description: L2 → L1 withdrawals for ETH and ERC-20 with quote, prepare, create, status, wait, and finalize helpers (Viem adapter). +group: API Reference / Viem +--- + +## At a glance + +- **Resource:** `sdk.withdrawals` +- **Typical flow:** `quote → create → wait({ for: 'l2' }) → wait({ for: 'ready' }) → finalize` +- **Auto-routing:** ETH vs ERC-20 and base-token vs non-base handled internally +- **Error style:** Throwing methods (`quote`, `prepare`, `create`, `status`, `wait`, `finalize`) + result variants (`tryQuote`, `tryPrepare`, `tryCreate`, `tryWait`, `tryFinalize`) + +## Import + +```ts +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; + +const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); +const l1 = createPublicClient({ transport: http(L1_RPC) }); +const l2 = createPublicClient({ transport: http(L2_RPC) }); +const l1Wallet: WalletClient = createWalletClient({ + account, + transport: http(L1_RPC), +}); + +// --- 2. Initialize the SDK --- +const client = createViemClient({ l1, l2, l1Wallet }); +const sdk = createViemSdk(client); +// sdk.withdrawals → WithdrawalsResource +``` + +## Quick start + +Withdraw **0.1 ETH** from L2 → L1 and finalize on L1: + +```ts +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: await signer.getAddress(), // L1 recipient +}); + +// 1) L2 inclusion (adds l2ToL1Logs if available) +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// 2) Wait until finalizable (no side effects) +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000 }); + +// 3) Finalize on L1 (no-op if already finalized) +const { status, receipt: l1Receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +``` + + +Withdrawals are two-phase: inclusion on **L2**, then **finalization on L1**. You can call finalize directly; it will throw if not yet ready. Prefer `wait(..., { for: 'ready' })` to avoid that. + + +## Route selection (automatic) + +- `eth-base` — Base token is **ETH** on L2 +- `eth-nonbase` — Base token is **not ETH** on L2 +- `erc20-nonbase` — Withdrawing an ERC-20 that is **not** the base token + +You **do not** pass a route; it’s derived from network metadata + `token`. + +## Method reference + +### `quote(p: WithdrawParams) → Promise` + +Estimate the operation (route, approvals, gas hints). Does **not** send txs. + + + L2 token (ETH handled by the SDK; ETH sentinel supported). + + + Amount in wei. + + + L1 recipient. + + +**Returns:** `WithdrawQuote` + +```ts +const q = await sdk.withdrawals.quote({ token, amount, to }); +/* +{ + route: "eth-base" | "eth-nonbase" | "erc20-nonbase", + approvalsNeeded: [{ token, spender, amount }], + suggestedL2GasLimit?: bigint +} +*/ +``` + +### `tryQuote(p) → Promise<{ ok: true; value: WithdrawQuote } | { ok: false; error }>` + +Result-style `quote`. + +### `prepare(p: WithdrawParams) → Promise>` + +Builds the plan (ordered L2 steps + unsigned txs) without sending. + +**Returns:** `WithdrawPlan` + +```ts +const plan = await sdk.withdrawals.prepare({ token, amount, to }); +/* +{ + route, + summary: WithdrawQuote, + steps: [ + { key, kind, tx: TransactionRequest }, + // … + ] +} +*/ +``` + +### `tryPrepare(p) → Promise<{ ok: true; value: WithdrawPlan } | { ok: false; error }>` + +Result-style `prepare`. + +### `create(p: WithdrawParams) → Promise>` + +Prepares and **executes** required **L2** steps. Returns a handle with the **L2 tx hash**. + +**Returns:** `WithdrawHandle` + +```ts +const handle = await sdk.withdrawals.create({ token, amount, to }); +/* +{ + kind: "withdrawal", + l2TxHash: Hex, + stepHashes: Record, + plan: WithdrawPlan +} +*/ +``` + + + If any L2 step reverts, create throws a typed error. Prefer tryCreate{' '} + for a result object. + + +### `tryCreate(p) → Promise<{ ok: true; value: WithdrawHandle } | { ok: false; error }>` + +Result-style `create`. + +### `status(handleOrHash) → Promise` + +Report current phase for a withdrawal. Accepts the `WithdrawHandle` from `create` **or** a raw **L2 tx hash**. + +**Phases** + +- `UNKNOWN` — no L2 hash provided +- `L2_PENDING` — L2 receipt missing +- `PENDING` — included on L2 but not yet finalizable +- `READY_TO_FINALIZE` — can be finalized on L1 now +- `FINALIZED` — already finalized on L1 + +```ts +const s = await sdk.withdrawals.status(handle); +// { phase, l2TxHash, key? } +``` + +### `wait(handleOrHash, { for: 'l2' | 'ready' | 'finalized', pollMs?, timeoutMs? })` + +Block until a target is reached. + +- `{ for: 'l2' }` → resolves **L2 receipt** (`TransactionReceiptZKsyncOS`) or `null` +- `{ for: 'ready' }` → resolves `null` when finalizable +- `{ for: 'finalized' }` → resolves **L1 receipt** (if found) or `null` + +```ts +const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000, timeoutMs: 15 * 60_000 }); +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', pollMs: 7000 }); +``` + + + Default polling is 5500 ms (min 1000 ms). Use timeoutMs for long windows. + + +### `tryWait(handleOrHash, opts) → Result` + +Result-style `wait`. + +### `finalize(l2TxHash: Hex) → Promise<{ status: WithdrawalStatus; receipt?: TransactionReceipt }>` + +Sends the **L1 finalize** transaction **if** ready. If already finalized, returns the status without sending. + +```ts +const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +if (status.phase === 'FINALIZED') { + console.log('L1 tx:', receipt?.transactionHash); +} +``` + + +If not ready, finalize throws a typed STATE error. Use status(...) or `wait(..., { for: 'ready' })` first to avoid throws. + + +### `tryFinalize(l2TxHash) → Promise<{ ok: true; value: { status: WithdrawalStatus; receipt?: TransactionReceipt } } | { ok: false; error }>` + +Result-style `finalize`. + +## End-to-end examples + +### Minimal happy path + +```ts +const handle = await sdk.withdrawals.create({ token, amount, to }); + +// L2 inclusion +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// Option A: finalize immediately (will throw if not ready) +await sdk.withdrawals.finalize(handle.l2TxHash); + +// Option B: wait for readiness, then finalize +await sdk.withdrawals.wait(handle, { for: 'ready' }); +await sdk.withdrawals.finalize(handle.l2TxHash); +``` + +## Types (overview) + +```ts +type WithdrawParams = { + token: Address; // L2 token (ETH sentinel supported) + amount: bigint; // wei + to: Address; // L1 recipient +}; + +type WithdrawQuote = { + route: 'eth-base' | 'eth-nonbase' | 'erc20-nonbase'; + approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; + suggestedL2GasLimit?: bigint; +}; + +type WithdrawPlan = { + route: WithdrawQuote['route']; + summary: WithdrawQuote; + steps: Array<{ key: string; kind: string; tx: TTx }>; +}; + +type WithdrawHandle = { + kind: 'withdrawal'; + l2TxHash: Hex; + stepHashes: Record; + plan: WithdrawPlan; +}; + +type WithdrawalStatus = + | { phase: 'UNKNOWN'; l2TxHash: Hex } + | { phase: 'L2_PENDING'; l2TxHash: Hex } + | { phase: 'PENDING'; l2TxHash: Hex; key?: unknown } + | { phase: 'READY_TO_FINALIZE'; l2TxHash: Hex; key: unknown } + | { phase: 'FINALIZED'; l2TxHash: Hex; key: unknown }; + +// L2 receipt augmentation returned by wait({ for: 'l2' }) +type TransactionReceiptZKsyncOS = TransactionReceipt & { l2ToL1Logs?: Array }; +``` + +## Notes & pitfalls + +- **Two chains, two receipts:** inclusion on **L2** and finalization on **L1** are independent events. +- **Polling strategy:** for production UIs, prefer `wait({ for: 'ready' })` then finalize; it avoids premature finalize calls. +- **Approvals:** if withdrawing ERC-20 requires approvals, `create` will include those steps automatically. diff --git a/docs/book.toml b/docs/book.toml deleted file mode 100644 index 1e197e4..0000000 --- a/docs/book.toml +++ /dev/null @@ -1,18 +0,0 @@ -[book] -title = "ZKsync SDK" -author = "Matter Labs" -language = "en" -multilingual = false -src = "src" - -[build] -build-dir = "book" # output folder (docs/book) - -[preprocessor.admonish] -command = "mdbook-admonish" -assets_version = "3.0.3" # do not edit: managed by `mdbook-admonish install` - -[output.html] -additional-css = ["./mdbook-admonish.css"] -default-theme = "light" -preferred-dark-theme = "navy" diff --git a/docs/changelog.mdx b/docs/changelog.mdx new file mode 100644 index 0000000..a5975f3 --- /dev/null +++ b/docs/changelog.mdx @@ -0,0 +1,95 @@ +--- +title: Changelog +description: New releases and improvements for the ZKsyncOS SDK. +rss: true +--- + + + ### Features - Add **custom base token** support + ([#39](https://github.com/dutterbutter/zksync-sdk/issues/39)) + ([a1bab03](https://github.com/dutterbutter/zksync-sdk/commit/a1bab034e6d133c74c43422533d5780165608e3f)) + + + Compare diff + + + + + + ### Features + - E2E tests for **ERC-20** across **ethers** and **viem** adapters ([#38](https://github.com/dutterbutter/zksync-sdk/issues/38)) ([763587a](https://github.com/dutterbutter/zksync-sdk/commit/763587a3035da61d7764864efe1048e8f4144062)) + +### Bug Fixes + +- Reported **code coverage** ([#32](https://github.com/dutterbutter/zksync-sdk/issues/32)) ([98834dc](https://github.com/dutterbutter/zksync-sdk/commit/98834dc722559e38da270395591145f4d91ea2e4)) +- Docs tweak ([#28](https://github.com/dutterbutter/zksync-sdk/issues/28)) ([533eb70](https://github.com/dutterbutter/zksync-sdk/commit/533eb70a61c33718a3f56850ba8db65a7f3204af)) +- CI env for E2E tests ([#31](https://github.com/dutterbutter/zksync-sdk/issues/31)) ([5d4df64](https://github.com/dutterbutter/zksync-sdk/commit/5d4df64c7a3307ec02ff9d6158f6b535af4f98b5)) +- Published build/types fixes ([#35](https://github.com/dutterbutter/zksync-sdk/issues/35)) ([901a0ea](https://github.com/dutterbutter/zksync-sdk/commit/901a0ea717e16076323f5c37b6e98ca5b2540578)) +- Ethers ERC-20 withdrawal overload fix ([#37](https://github.com/dutterbutter/zksync-sdk/issues/37)) ([2403a9c](https://github.com/dutterbutter/zksync-sdk/commit/2403a9c122e7a6e8c1f24cd407eef57abf3b076a)) + + Compare diff + + + + ### Bug Fixes - Adjust READMEs ([#25](https://github.com/dutterbutter/zksync-sdk/issues/25)) + ([eacd5ae](https://github.com/dutterbutter/zksync-sdk/commit/eacd5ae6f27332ad8c756d67276e24fbdd3187df)) + + + Compare diff + + + + + + ### Bug Fixes - Remove JSON in favor of TS + ([#23](https://github.com/dutterbutter/zksync-sdk/issues/23)) + ([dca83b6](https://github.com/dutterbutter/zksync-sdk/commit/dca83b6e34c7dbf0d866e27dc9c7fa4f58bc5656)) + + + Compare diff + + + + + + ### Bug Fixes - Additional build fixes + ([#21](https://github.com/dutterbutter/zksync-sdk/issues/21)) + ([65475b2](https://github.com/dutterbutter/zksync-sdk/commit/65475b23b0acf8bbf8454e1ff39f59a09fd68aa9)) + + + Compare diff + + + + + + ### Features + - Release & publishing pipeline ([#4](https://github.com/dutterbutter/zksync-sdk/issues/4)) ([d253c8c](https://github.com/dutterbutter/zksync-sdk/commit/d253c8c19ac0184af6825764ade7b23a14bf6798)) + - **Status polling** for deposits ([5eefe8d](https://github.com/dutterbutter/zksync-sdk/commit/5eefe8d83a5d674cdf486cef2e4467507dcf6d20)) + - **Ethers** adapter unit tests ([fccb3a5](https://github.com/dutterbutter/zksync-sdk/commit/fccb3a56dd380626af93d16b36b8bd68441159a2)) + - **viem** support ([3a5c559](https://github.com/dutterbutter/zksync-sdk/commit/3a5c5598b49f909b334c597f06d18678155fdf5f)) + - Docs scaffolding ([ab152df](https://github.com/dutterbutter/zksync-sdk/commit/ab152df33d57f0e1567742f0f5bef256c2974f44)) + - Deposit flow + `wait()` improvements ([de6d6c0](https://github.com/dutterbutter/zksync-sdk/commit/de6d6c0ce391fe29f9c92603b9cbc2e088dbfe8a)) + - Deposits in **viem** ([e7267cb](https://github.com/dutterbutter/zksync-sdk/commit/e7267cb001f06ffbafadfea3dbe7a935375fcb2d)) + - E2E test cleanups + docs ([252b18a](https://github.com/dutterbutter/zksync-sdk/commit/252b18adce979dc337fc81a993c216d4592082af)) + - E2E tests for **ETH** ([8e7d453](https://github.com/dutterbutter/zksync-sdk/commit/8e7d453493202e605f0f8b95e9c0a3cf99fdfea4), [31b43c6](https://github.com/dutterbutter/zksync-sdk/commit/31b43c62f70731f2c762214cb63689c0c6e44094)) + - **Error envelope** groundwork + integration ([0c9ea07](https://github.com/dutterbutter/zksync-sdk/commit/0c9ea078d3130095896406c943b363d1ac476e43), [e496cc8](https://github.com/dutterbutter/zksync-sdk/commit/e496cc8a98cfe7e3512288ff861110c34ede04e0)) + - **Finalization** interfacing completed ([a026203](https://github.com/dutterbutter/zksync-sdk/commit/a0262033809cb8505a20511edc39083f820a439a)) + - Prep for alpha.1 ([c6c15e1](https://github.com/dutterbutter/zksync-sdk/commit/c6c15e12fba16a355171e30db42995600fad106b)) + - E2E green ✅ ([c65c5b1](https://github.com/dutterbutter/zksync-sdk/commit/c65c5b1976a940ac8f8ff4f82cb7b74cd8d37f5b)) + +### Bug Fixes + +- Export fixes in build ([#18](https://github.com/dutterbutter/zksync-sdk/issues/18)) ([5afbcbd](https://github.com/dutterbutter/zksync-sdk/commit/5afbcbdf13a3e15da94c8b66bc38e643097f917a)) +- Build stability improvements ([1a24cf7](https://github.com/dutterbutter/zksync-sdk/commit/1a24cf76d61ee9c172fb0428c5b2386f4553f736)) + + Compare diff + + + + ### Highlights + - **Initial alpha** release: core flows, **viem** + **ethers** adapters, deposits/withdrawals, status/wait helpers. + - CI/release scaffolding and early docs. + + Compare diff + diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 0000000..ae6234d --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,163 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "aspen", + "name": "ZKsync SDK", + "colors": { + "primary": "#1755F4", + "light": "#BFF351", + "dark": "#0C18EC" + }, + "favicon": "/favicon.svg", + "appearance": { + "default": "dark" + }, + "styling": { + "eyebrows": "breadcrumbs", + "codeblocks": { + "theme": { + "light": "vitesse-light", + "dark": "tokyo-night" + } + } + }, + "navigation": { + "tabs": [ + { + "tab": "Home", + "icon": "house", + "groups": [ + { + "group": "Overview", + "pages": ["index"] + } + ] + }, + { + "tab": "Getting Started", + "icon": "play", + "groups": [ + { + "group": "Overview", + "pages": [ + "overview/index", + "overview/mental-model", + "overview/adapters", + "overview/status-vs-wait", + "overview/finalization" + ] + }, + { + "group": "Quickstart", + "pages": ["quickstart/index", "quickstart/deposits", "quickstart/withdrawals"] + }, + { + "group": "Guides", + "pages": ["guides/deposits", "guides/withdrawals"] + } + ] + }, + { + "tab": "SDK Reference", + "icon": "brackets-curly", + "groups": [ + { + "group": "Overview", + "pages": ["api-reference/introduction"] + }, + { + "group": "Ethers", + "pages": [ + "api-reference/ethers/client", + "api-reference/ethers/sdk", + "api-reference/ethers/deposits", + "api-reference/ethers/withdrawals" + ] + }, + { + "group": "Viem", + "pages": [ + "api-reference/viem/client", + "api-reference/viem/sdk", + "api-reference/viem/deposits", + "api-reference/viem/withdrawals" + ] + }, + { + "group": "Core", + "pages": ["api-reference/core/rpc", "api-reference/core/errors"] + } + ] + }, + { + "tab": "Changelog", + "icon": "calendar", + "groups": [ + { + "group": "Releases", + "pages": ["changelog"] + } + ] + } + ] + }, + "logo": { + "light": "/logo/zksync-dark.svg", + "dark": "/logo/zksync-light.svg", + "href": "https://zksync.io" + }, + "search": { + "prompt": "Search ZKsync SDK..." + }, + "navbar": { + "links": [ + { + "label": "Support", + "href": "https://github.com/zkSync-Community-Hub/zksync-developers/discussions/categories/sdks", + "icon": "arrow-up-right-from-square" + }, + { + "label": "Examples", + "href": "https://github.com/dutterbutter/zksync-sdk/tree/main/examples", + "icon": "arrow-up-right-from-square" + } + ] + }, + "contextual": { + "options": ["copy", "view", "chatgpt", "claude", "perplexity", "mcp", "cursor", "vscode"] + }, + "footer": { + "socials": { + "x": "https://x.com/zksync", + "github": "https://github.com/dutterbutter/zksync-sdk", + "linkedin": "https://linkedin.com/company/matter-labs" + }, + "links": [ + { + "header": "Community", + "items": [ + { + "label": "Discord", + "href": "https://join.zksync.dev/" + }, + { + "label": "GitHub Discussions", + "href": "https://github.com/zkSync-Community-Hub/zksync-developers/discussions" + } + ] + }, + { + "header": "Resources", + "items": [ + { + "label": "Block Explorer", + "href": "https://explorer.zksync.io/" + }, + { + "label": "Website", + "href": "https://zksync.io/" + } + ] + } + ] + } +} diff --git a/docs/favicon.svg b/docs/favicon.svg new file mode 100644 index 0000000..2791d9f --- /dev/null +++ b/docs/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/guides/deposits.mdx b/docs/guides/deposits.mdx new file mode 100644 index 0000000..5e46bc1 --- /dev/null +++ b/docs/guides/deposits.mdx @@ -0,0 +1,408 @@ +--- +title: 'Deposits (L1 → L2)' +description: 'Fast, developer-focused deposits with validation, observability, and battle-tested patterns.' +--- + +# Guide: Depositing funds L1 to L2 + +This guide provides a complete walkthrough for depositing funds from a L1 (Ethereum) to a L2 (ZKsync). We will use the `@dutterbutter/zksync-sdk` to simplify the process. + +The SDK intelligently handles the deposit flow, automatically selecting the correct contract and route based on the token being used. + +### Prerequisites + +Before you begin, ensure you have the following: + +1. **Node.js v20+** or **Bun.sh** installed. + +2. An L1 wallet (private key) with some ETH to pay for the deposit and gas fees. + +3. RPC endpoints for both the L1 and L2 you are targeting. + +4. A project set up with `viem` or `ethers` and `@dutterbutter/zksync-sdk` installed. + + + ```bash title="viem" npm install viem @dutterbutter/zksync-sdk dotenv ``` ```bash + title="ethers" npm install ethers @dutterbutter/zksync-sdk dotenv ``` + + +5. An `.env` file in your project's root directory with the following variables: + + ```ini + # .env + L1_RPC_URL="YOUR_L1_RPC_ENDPOINT" + L2_RPC_URL="YOUR_L2_RPC_ENDPOINT" + PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY" + ``` + +### The Deposit Process: Step-by-Step + +The process can be broken down into 5 key steps after initial setup. + +#### Step 0: Setup and SDK Initialization + +First, we need to connect to the L1 and L2 networks using your preferred library (`viem` or `ethers`) and then initialize the ZKsync SDK. + + +```typescript title="viem" +import 'dotenv/config'; +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +// Load configuration from .env file +const L1_RPC = process.env.L1_RPC_URL!; +const L2_RPC = process.env.L2_RPC_URL!; +const PRIVATE_KEY = process.env.PRIVATE_KEY!; + +// --- Clients (Viem) --- +const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); +const l1 = createPublicClient({ transport: http(L1_RPC) }); +const l2 = createPublicClient({ transport: http(L2_RPC) }); +const l1Wallet: WalletClient = createWalletClient({ +account, +transport: http(L1_RPC), +}); + +// --- 2. Initialize the SDK --- +const client = createViemClient({ l1, l2, l1Wallet }); +const sdk = createViemSdk(client); + +console.log(`Using account: ${account.address}`); + +```` +```typescript title="ethers" +import 'dotenv/config'; +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +// Load configuration from .env file +const L1_RPC = process.env.L1_RPC_URL!; +const L2_RPC = process.env.L2_RPC_URL!; +const PRIVATE_KEY = process.env.PRIVATE_KEY!; + +// --- 1. Initialize Providers and Signer --- +const l1Provider = new JsonRpcProvider(L1_RPC); +const l2Provider = new JsonRpcProvider(L2_RPC); +const signer = new Wallet(PRIVATE_KEY, l1Provider); + +// --- 2. Initialize the SDK --- +const client = await createEthersClient({ l1: l1Provider, l2: l2Provider, signer }); +const sdk = createEthersSdk(client); + +console.log(`Using account: ${signer.address}`); +```` + + + +#### Step 1: Define Deposit Parameters + +Next, define the parameters for your deposit in an object. You’ll specify the deposit amount, the token address — which can represent ETH, a chain’s Base Token (like L1_SOPH_TOKEN_ADDRESS), or any ERC-20 — and the recipient’s address on L2. + + +```typescript title="ETH Token Deposit" +const me = signer.address as Address; + +const depositParams = { +amount: parseEther('0.01'), // The amount of ETH to deposit (e.g., 0.01 ETH) +token: ETH_ADDRESS, // The address for native ETH +to: me, // The recipient address on L2 (in this case, our own address) +} as const; + +```` +```typescript title="ERC20 Token Deposit" +const me = signer.address as Address; + +const depositParams = { + amount: parseEther('0.01'), // The amount of ERC20 to deposit (e.g., 0.01 ERC20) + token: ERC20_TOKEN_ADDRESS, // The address for the ERC20 token + to: me, // The recipient address on L2 (in this case, our own address) +} as const; +```` + +```typescript title="Base Token Deposit" +const me = signer.address as Address; + +const depositParams = { + amount: parseEther('0.01'), // The amount of SOPH to deposit (e.g., 0.01 SOPH) + token: L1_SOPH_TOKEN_ADDRESS, // The address for SOPH (or your chain's base token on L1) + to: me, // The recipient address on L2 (in this case, our own address) +} as const; +``` + + + + + +You can also specify advanced options for finer control over the transaction: + +- `l2GasLimit`: The gas limit for the L2 part of the transaction. +- `gasPerPubdata`: The gas price per byte of public data. +- `operatorTip`: A tip for the L2 operator. +- `refundRecipient`: An address to receive any L1 gas refunds. + + + +#### Step 2: Quote the Deposit + +Before sending, it's best practice to get a cost estimate using `sdk.deposits.quote`. This returns information about fees and the expected gas required. + + + Users can skip this step and go directly to `create`, but quoting helps avoid surprises. + + +```typescript +// --- STEP 2: QUOTE --- +const quote = await sdk.deposits.quote(depositParams); +console.log('DEPOSIT QUOTE →', quote); +// Outputs estimated costs and gas limits +``` + +#### Step 3: Prepare the Transaction + +The `sdk.deposits.prepare` method builds the final transaction plan. This step is useful for inspecting the transaction details before execution. + +```typescript +// --- STEP 3: PREPARE --- +const plan = await sdk.deposits.prepare(depositParams); +console.log('TRANSACTION PLAN →', plan); +// Outputs the prepared transaction objects +``` + +#### Step 4: Create the Deposit + +The `sdk.deposits.create` method executes the plan by sending the transaction to the L1 network. It returns a `handle` which is a unique identifier used to track the deposit's progress. + +```typescript +// --- STEP 4: CREATE (send tx) --- +const handle = await sdk.deposits.create(depositParams); +console.log('TRANSACTION CREATED →', handle); +// Outputs the handle, e.g., { type: 'deposit', hash: '0x...' } +``` + +#### Step 5: Track the Transaction Status + +A deposit is a two-stage process: inclusion on L1 and then execution on L2. The SDK provides tools to track both stages. + +- **`sdk.deposits.status(handle)`:** Check the current status of the deposit at any time. +- **`sdk.deposits.wait(handle, options)`:** Pause execution until the deposit reaches a specific stage. + +```typescript +// --- STEP 5: TRACK --- +console.log('⏳ Waiting for L1 inclusion...'); +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +console.log('✅ L1 included at block:', l1Receipt?.blockNumber); + +// Check the status again after L1 inclusion +const statusAfterL1 = await sdk.deposits.status(handle); +console.log('STATUS (after L1) →', statusAfterL1); + +console.log('⏳ Waiting for L2 execution...'); +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +console.log('✅ L2 executed at block:', l2Receipt?.blockNumber); +``` + +Once the `wait` for L2 execution completes, your ETH has successfully been deposited to your L2 address. + +### Full Code Example + +Here is the complete, runnable script for your reference. + + + + + +```typescript title="viem" +/** + * Example: Deposit ETH into an L2 + */ +import 'dotenv/config'; +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +// --- CONFIG --- +// Use env if available, otherwise fall back to local dev defaults. +const L1_RPC = process.env.L1_RPC_URL ?? 'http://localhost:8545'; +const L2_RPC = process.env.L2_RPC_URL ?? 'http://localhost:3050'; +const PRIVATE_KEY = process.env.PRIVATE_KEY ?? ''; + +async function main() { + if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66) { + throw new Error('⚠️ Set a 0x-prefixed 32-byte PRIVATE_KEY in your .env'); + } + + // --- Clients (Viem) --- + const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); + const l1 = createPublicClient({ transport: http(L1_RPC) }); + const l2 = createPublicClient({ transport: http(L2_RPC) }); + + const l1Wallet: WalletClient = createWalletClient({ + account, + transport: http(L1_RPC), + }); + + // Show balances for sanity + const [balL1, balL2] = await Promise.all([ + l1.getBalance({ address: account.address }), + l2.getBalance({ address: account.address }), + ]); + console.log('Using account:', account.address); + console.log('L1 balance:', balL1.toString()); + console.log('L2 balance:', balL2.toString()); + + // --- Init SDK --- + const client = createViemClient({ l1, l2, l1Wallet }); + const sdk = createViemSdk(client); + + // --- Deposit params --- + const me = account.address as Address; + const params = { + amount: parseEther('0.01'), + token: ETH_ADDRESS, // ETH sentinel + to: me, + // optional: + // l2GasLimit: 300_000n, + // gasPerPubdata: 800n, + // operatorTip: 0n, + // refundRecipient: me, + } as const; + + // 1) QUOTE + const quote = await sdk.deposits.quote(params); + console.log('QUOTE →', quote); + + // 2) PREPARE (no txs sent) + const plan = await sdk.deposits.prepare(params); + console.log('PREPARE →', plan); + + // 3) CREATE (send deposit) + const handle = await sdk.deposits.create(params); + console.log('CREATE →', handle); + + // 4) STATUS + const status = await sdk.deposits.status(handle); + console.log('STATUS →', status); + + // 5) WAIT: L1 inclusion + console.log('⏳ Waiting for L1 inclusion...'); + const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); + console.log('✅ L1 included at block:', l1Receipt?.blockNumber); + + const statusAfterL1 = await sdk.deposits.status(handle); + console.log('STATUS (after L1) →', statusAfterL1); + + // 6) WAIT: L2 execution + console.log('⏳ Waiting for L2 execution...'); + const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); + console.log('✅ L2 executed at block:', l2Receipt?.blockNumber); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +```typescript title="ethers" +/** + * Example: Deposit ETH from L1 into a ZKsync L2 + */ +import 'dotenv/config'; +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +// --- CONFIG --- +const L1_RPC = process.env.L1_RPC_URL ?? 'http://localhost:8545'; +const L2_RPC = process.env.L2_RPC_URL ?? 'http://localhost:3050'; +const PRIVATE_KEY = process.env.PRIVATE_KEY ?? ''; + +async function main() { + if (!PRIVATE_KEY) throw new Error('⚠️ Set your PRIVATE_KEY in the .env file'); + + const l1 = new JsonRpcProvider(L1_RPC); + const l2 = new JsonRpcProvider(L2_RPC); + const signer = new Wallet(PRIVATE_KEY, l1); + + const me = signer.address as Address; + console.log('Using account:', me); + + // --- INIT SDK --- + const client = await createEthersClient({ l1, l2, signer }); + const sdk = createEthersSdk(client); + + // --- DEPOSIT PARAMS --- + const params = { + amount: parseEther('0.01'), + token: ETH_ADDRESS, + to: me, + } as const; + + // --- STEP 1: QUOTE --- + const quote = await sdk.deposits.quote(params); + console.log('QUOTE →', quote); + + // --- STEP 2: PREPARE --- + const plan = await sdk.deposits.prepare(params); + console.log('PREPARE →', plan); + + // --- STEP 3: CREATE (send tx) --- + const handle = await sdk.deposits.create(params); + console.log('CREATE →', handle); + + // --- STEP 4 & 5: TRACK --- + console.log('⏳ Waiting for L1 inclusion...'); + const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); + console.log('✅ L1 included at block:', l1Receipt?.blockNumber); + + console.log('⏳ Waiting for L2 execution...'); + const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); + console.log('✅ L2 executed at block:', l2Receipt?.blockNumber); + + const finalStatus = await sdk.deposits.status(handle); + console.log('FINAL STATUS →', finalStatus); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + +### Related API Reference + +For more detailed information on the methods used in this guide, see the official API reference: + +- [`sdk.deposits.quote()`](https://zksync-sdk.mintlify.app/api-reference/ethers/deposits#quote-p%3A-depositparams-%E2%86%92-promise%3Cdepositquote%3E) +- [`sdk.deposits.prepare()`](https://zksync-sdk.mintlify.app/api-reference/ethers/deposits#prepare-p%3A-depositparams-%E2%86%92-promise%3Cdepositplan%3Ctransactionrequest%3E%3E) +- [`sdk.deposits.create()`](https://zksync-sdk.mintlify.app/api-reference/ethers/deposits#create-p%3A-depositparams-%E2%86%92-promise%3Cdeposithandle%3Ctransactionrequest%3E%3E) +- [`sdk.deposits.status()`](https://zksync-sdk.mintlify.app/api-reference/ethers/deposits#status-handleorhash-%E2%86%92-promise%3Cdepositstatus%3E) +- [`sdk.deposits.wait()`](https://zksync-sdk.mintlify.app/api-reference/ethers/deposits#wait-handleorhash%2C-%7B-for%3A-l1-%7C-l2-%7D-%E2%86%92-promise%3Ctransactionreceipt-%7C-null%3E) diff --git a/docs/guides/withdrawals.mdx b/docs/guides/withdrawals.mdx new file mode 100644 index 0000000..676482e --- /dev/null +++ b/docs/guides/withdrawals.mdx @@ -0,0 +1,408 @@ +--- +title: 'Withdrawals (L2 → L1)' +description: 'Fast, developer-focused withdrawals with validation, observability, and battle-tested patterns.' +--- + +# Guide: Withdrawing funds L2 to L1 + +This guide provides a complete walkthrough for withdrawing funds from an L2 (ZKsync) back to L1 (Ethereum). We will use the `@dutterbutter/zksync-sdk` to simplify the process. + +The SDK intelligently handles the withdrawal flow, automatically selecting the correct route and approval requirements based on the token being used. + +### Prerequisites + +Before you begin, ensure you have the following: + +1. **Node.js v20+** or **Bun.sh** installed. + +2. An account with **sufficient L2 balance** of the asset you want to withdraw and **L2 gas** for the withdrawal transaction. + +3. RPC endpoints for both the **L2** you’re withdrawing from and the **L1** you’re withdrawing to. + +4. A project set up with `viem` or `ethers` and `@dutterbutter/zksync-sdk` installed. + + + ```bash title="viem" npm install viem @dutterbutter/zksync-sdk dotenv ``` ```bash + title="ethers" npm install ethers @dutterbutter/zksync-sdk dotenv ``` + + +5. An `.env` file in your project's root directory with the following variables: + + ```ini + # .env + L1_RPC_URL="YOUR_L1_RPC_ENDPOINT" + L2_RPC_URL="YOUR_L2_RPC_ENDPOINT" + PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY" + ``` + +### The Withdrawal Process: Step-by-Step + +Withdrawals generally follow **two phases**: + +- An **L2 transaction** that burns/transfers the token and emits the L2→L1 message. +- A **finalization on L1** (after the message is ready) to release funds on L1. + The SDK exposes `wait(..., { for: 'ready' })`, `tryFinalize(...)`, and `wait(..., { for: 'finalized' })`. + +#### Step 0: Setup and SDK Initialization + +Connect to L1 and L2 using your preferred library (`viem` or `ethers`) and initialize the ZKsync SDK. + + +```typescript title="viem" +import 'dotenv/config'; +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; + +// Load configuration from .env file +const L1_RPC = process.env.L1_RPC_URL!; +const L2_RPC = process.env.L2_RPC_URL!; +const PRIVATE_KEY = process.env.PRIVATE_KEY!; + +// --- 1. Initialize Clients and Wallet --- +const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); +const l1 = createPublicClient({ transport: http(L1_RPC) }); +const l2 = createPublicClient({ transport: http(L2_RPC) }); +const l1Wallet: WalletClient = createWalletClient({ +account, +transport: http(L1_RPC), +}); +// Need to provide an L2 wallet client for sending L2 withdraw tx +const l2Wallet = createWalletClient({ +account, +transport: http(L2_RPC), +}); + +// --- 2. Initialize the SDK --- +const client = createViemClient({ l1, l2, l2Wallet }); +const sdk = createViemSdk(client); + +console.log(`Using account: ${account.address}`); + +```` +```typescript title="ethers" +import 'dotenv/config'; +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; + +// Load configuration from .env file +const L1_RPC = process.env.L1_RPC_URL!; +const L2_RPC = process.env.L2_RPC_URL!; +const PRIVATE_KEY = process.env.PRIVATE_KEY!; + +// --- 1. Initialize Providers and Signer --- +// You’ll send the withdraw tx on L2. +const l1Provider = new JsonRpcProvider(L1_RPC); +const l2Provider = new JsonRpcProvider(L2_RPC); +const signer = new Wallet(PRIVATE_KEY, l1Provider); + +// --- 2. Initialize the SDK --- +const client = await createEthersClient({ l1: l1Provider, l2: l2Provider, signer }); +const sdk = createEthersSdk(client); + +console.log(`Using account: ${await signer.getAddress()}`); +```` + + + +#### Step 1: Define Withdrawal Parameters + +Next, define the parameters for your withdrawal in an object. You’ll specify the **amount**, the **L2 token address** (or `ETH_ADDRESS` for native ETH on ETH-based L2s), and the **recipient’s address on L1**. + + +```typescript title="ETH — Base L2" +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; + +const withdrawalParams = { +amount: parseEther('0.02'), +token: ETH_ADDRESS, // 👈 For ETH-based L2s, use the ETH sentinel +to: await signer.getAddress(), // L1 recipient (can be different from the L2 sender) +} as const; + +```` + +```typescript title="ETH — Non-Base L2" +import type { Address } from '@dutterbutter/zksync-sdk/core'; + +// Example: an L2-ETH token address on your L2 (replace with the correct L2 token) +// You can store/resolve this via config or a helper. +const L2_ETH_TOKEN = process.env.L2_ETH_TOKEN as Address; + +const withdrawalParams = { + amount: parseEther('0.02'), + token: L2_ETH_TOKEN, // 👈 L2 representation of ETH on non-ETH-base chains + to: await signer.getAddress(), +} as const; +```` + +```typescript title="ERC-20" +import type { Address } from '@dutterbutter/zksync-sdk/core'; + +// If you have an L1 token and need its L2 counterpart: +// const l2Token = await sdk.helpers.l2TokenAddress(L1_ERC20_TOKEN); +// Otherwise, use your known L2 token address directly: +const L2_ERC20_TOKEN = process.env.L2_ERC20_TOKEN as Address; + +const withdrawalParams = { + amount: parseEther('10'), // or use parseUnits('amount', decimals) if not 18 + token: L2_ERC20_TOKEN, // 👈 L2 token address for the ERC-20 + to: await signer.getAddress(), +} as const; +``` + +```typescript title="Base Token" +import { L2_BASE_TOKEN_ADDRESS } from '@dutterbutter/zksync-sdk/core'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; + +const withdrawalParams = { + amount: parseEther('5'), // adjust to your base token decimals if not 18 + token: L2_BASE_TOKEN_ADDRESS, // 👈 L2 address of the chain’s Base Token (ERC-20) + to: await signer.getAddress(), +} as const; +``` + + + + + +You can also specify advanced options for finer control over the transaction: + +- `l2GasLimit`: The gas limit for the L2 withdrawal transaction. +- `operatorTip`: A tip for the L2 operator (if supported). +- `refundRecipient`: An L1 address to receive any L1 finalize-phase refunds (chain-specific). + + + +#### Step 2: Quote the Withdrawal + +Before sending, get an estimate with `sdk.withdrawals.quote`. This returns expected fees and gas for the L2 step and any finalize phase hints. + +You can skip straight to `create`, but quoting helps avoid surprises. + +```typescript +// --- STEP 2: QUOTE --- +const quote = await sdk.withdrawals.quote(withdrawalParams); +console.log('WITHDRAW QUOTE →', quote); +``` + +#### Step 3: Prepare the Transaction + +Build the execution plan with `sdk.withdrawals.prepare`. For non-base ERC-20s or non-ETH L2-ETH, this may include **L2 approvals**. + +```typescript +// --- STEP 3: PREPARE --- +const plan = await sdk.withdrawals.prepare(withdrawalParams); +console.log('TRANSACTION PLAN →', plan); +``` + +#### Step 4: Create the Withdrawal + +Execute the plan with `sdk.withdrawals.create`. You’ll get a `handle` you can use to track the withdrawal across phases. + +```typescript +// --- STEP 4: CREATE (send L2 tx) --- +const handle = await sdk.withdrawals.create(withdrawalParams); +console.log('TRANSACTION CREATED →', handle); +``` + +#### Step 5: Track the Withdrawal Lifecycle + +Use `status` and `wait` to track all phases: + +- `wait(handle, { for: 'l2' })` → L2 inclusion +- `wait(handle, { for: 'ready' })` → message proven/ready on L1 +- `tryFinalize(l2TxHash)` → submit finalize on L1 (no-op if already finalized) +- `wait(l2TxHash, { for: 'finalized' })` → finalized on L1 + +```typescript +// --- STEP 5: TRACK --- +// L2 inclusion +console.log('⏳ Waiting for L2 inclusion...'); +const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); +console.log('✅ L2 included at block:', l2Receipt?.blockNumber); + +// Ready to finalize on L1 +console.log('⏳ Waiting until ready to finalize on L1...'); +await sdk.withdrawals.wait(handle, { for: 'ready' }); +console.log('STATUS (ready):', await sdk.withdrawals.status(handle)); + +// Submit finalize (safe to call even if someone else already finalized) +const finalizeResult = await sdk.withdrawals.tryFinalize(handle.l2TxHash); +console.log('TRY FINALIZE →', finalizeResult); + +// Confirm finalization +console.log('⏳ Waiting for L1 finalization...'); +const l1Receipt = await sdk.withdrawals.wait(handle.l2TxHash, { for: 'finalized' }); +console.log('✅ Finalized on L1. Receipt:', l1Receipt?.transactionHash ?? '(finalized elsewhere)'); +``` + +Once the `finalized` phase completes, your funds are available on L1 at the `to` address. + +### Full Code Example + +Here is the complete, runnable script for your reference. + + + + + +```typescript title="viem" +/** + * Example: Withdraw ETH (ETH-based L2) → L1 + */ +import 'dotenv/config'; +import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +const L1_RPC = process.env.L1_RPC_URL ?? 'http://localhost:8545'; +const L2_RPC = process.env.L2_RPC_URL ?? 'http://localhost:3050'; +const PRIVATE_KEY = process.env.PRIVATE_KEY ?? ''; + +async function main() { + if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66) { + throw new Error('⚠️ Set a 0x-prefixed 32-byte PRIVATE_KEY in your .env'); + } + + const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); + const l1 = createPublicClient({ transport: http(L1_RPC) }); + const l2 = createPublicClient({ transport: http(L2_RPC) }); + const l2Wallet: WalletClient = createWalletClient({ + account, + transport: http(L2_RPC), + }); + + const client = createViemClient({ l1, l2, l2Wallet }); + const sdk = createViemSdk(client); + + const meL1 = account.address as Address; + const params = { + amount: parseEther('0.02'), + token: ETH_ADDRESS, + to: meL1, + } as const; + + const quote = await sdk.withdrawals.quote(params); + console.log('QUOTE →', quote); + + const plan = await sdk.withdrawals.prepare(params); + console.log('PREPARE →', plan); + + const handle = await sdk.withdrawals.create(params); + console.log('CREATE →', handle); + + const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); + console.log('✅ L2 included at block:', l2Receipt?.blockNumber); + + await sdk.withdrawals.wait(handle, { for: 'ready' }); + console.log('STATUS (ready) →', await sdk.withdrawals.status(handle)); + + const fin = await sdk.withdrawals.tryFinalize(handle.l2TxHash); + console.log('TRY FINALIZE →', fin); + + const l1Receipt = await sdk.withdrawals.wait(handle.l2TxHash, { for: 'finalized' }); + console.log('✅ Finalized on L1:', l1Receipt?.transactionHash ?? '(finalized elsewhere)'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +```typescript title="ethers" +/** + * Example: Withdraw Base Token (ERC-20 base) or ETH → L1 + */ +import 'dotenv/config'; +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +import type { Address } from '@dutterbutter/zksync-sdk/core'; +import { ETH_ADDRESS, L2_BASE_TOKEN_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +const L1_RPC = process.env.L1_RPC_URL ?? 'http://localhost:8545'; +const L2_RPC = process.env.L2_RPC_URL ?? 'http://localhost:3050'; +const PRIVATE_KEY = process.env.PRIVATE_KEY ?? ''; + +async function main() { + if (!PRIVATE_KEY) throw new Error('⚠️ Set your PRIVATE_KEY in the .env file'); + + const l1 = new JsonRpcProvider(L1_RPC); + const l2 = new JsonRpcProvider(L2_RPC); + const signer = new Wallet(PRIVATE_KEY, l2); // withdraw is sent on L2 + + const client = await createEthersClient({ l1, l2, signer }); + const sdk = createEthersSdk(client); + + const meL1 = (await signer.getAddress()) as Address; + + // Toggle either ETH or the L2 base token address: + const params = { + amount: parseEther('1'), + token: ETH_ADDRESS /* or L2_BASE_TOKEN_ADDRESS */, + to: meL1, + } as const; + + const quote = await sdk.withdrawals.quote(params); + console.log('QUOTE →', quote); + + const plan = await sdk.withdrawals.prepare(params); + console.log('PREPARE →', plan); + + const handle = await sdk.withdrawals.create(params); + console.log('CREATE →', handle); + + const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); + console.log('✅ L2 included at block:', l2Receipt?.blockNumber); + + await sdk.withdrawals.wait(handle, { for: 'ready' }); + console.log('STATUS (ready) →', await sdk.withdrawals.status(handle)); + + const fin = await sdk.withdrawals.tryFinalize(handle.l2TxHash); + console.log('TRY FINALIZE →', fin); + + const l1Receipt = await sdk.withdrawals.wait(handle.l2TxHash, { for: 'finalized' }); + console.log('✅ Finalized on L1:', l1Receipt?.transactionHash ?? '(finalized elsewhere)'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + +### Related API Reference + +For more detailed information on the methods used in this guide, see the official API reference: + +- [`sdk.withdrawals.quote()`](/api-reference/ethers/withdrawals#quote-p%3A-withdrawparams-→-promise) +- [`sdk.withdrawals.prepare()`](/api-reference/ethers/withdrawals#prepare-p%3A-withdrawparams-→-promise>) +- [`sdk.withdrawals.create()`](/api-reference/ethers/withdrawals#create-p%3A-withdrawparams-→-promise>) +- [`sdk.withdrawals.status()`](/api-reference/ethers/withdrawals#status-handleorhash-→-promise) +- [`sdk.withdrawals.wait()`](/api-reference/ethers/withdrawals#wait-handleorhash%2C-%7B-for%3A-l2-%7C-ready-%7C-finalized-%2C-pollms%3F%2C-timeoutms%3F-%7D) +- [`sdk.withdrawals.tryFinalize()`](/api-reference/ethers/withdrawals#tryfinalize-l2txhash-→-promise<%7B-ok%3A-true%3B-value%3A-%7B-status%3A-withdrawalstatus%3B-receipt%3F%3A-transactionreceipt-%7D-%7D-%7C-%7B-ok%3A-false%3B-error-%7D>) diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..2de743e --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,93 @@ +--- +title: ZKsyncOS SDK +description: Lightweight adapters for viem and ethers to build ZKsync L1↔L2 flows fast. +mode: 'custom' +--- + +
+
+ {/* LEFT: Promo copy */} +
+
+ + Ready for the Atlas Upgrade +
+ +

+ Incorruptible + ZKsync power
+ in your favorite
libraries +

+ +

+ Extend viem and ethers with optimized L1↔L2 flows: + deposits, withdrawals, and clean status/wait helpers—tuned for the + Elastic Network and great DX. +

+ + +
+ + {/* RIGHT: Installer card */} +
+
+
Install with bun
+ ```bash + bun add @dutterbutter/zksync-sdk + ``` +
+ Prefer npm or pnpm? Try npm i or pnpm add. +
+
+
+ +
+
+ +
+

Quickstart guides

+ + + Install the `viem` or `ethers` adapter, connect clients, and run your first deposit. + + + Send ETH or ERC-20 from Ethereum to ZKsync, then track L1 inclusion and L2 execution. + + + Create the withdrawal on ZKsync, monitor execution, and finalize back on Ethereum. + + + Understand statuses vs waits and how to poll each phase of a flow. + + + Understand the finalization process for withdrawals and how to ensure they are completed. + + + Utilities like getBridgehubAddress, getL2ToL1LogProof. + + +
+ +
+

Resources

+ + + Fully typed clients, methods, and errors for core and adapters. + + + See what’s new, fixed, and changed across releases. + + + Questions or issues? Reach out and we’ll help you. + + +
diff --git a/docs/logo/zksync-dark.svg b/docs/logo/zksync-dark.svg new file mode 100644 index 0000000..1b42473 --- /dev/null +++ b/docs/logo/zksync-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/logo/zksync-light.svg b/docs/logo/zksync-light.svg new file mode 100644 index 0000000..1e28358 --- /dev/null +++ b/docs/logo/zksync-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/mdbook-admonish.css b/docs/mdbook-admonish.css deleted file mode 100644 index b6df9b0..0000000 --- a/docs/mdbook-admonish.css +++ /dev/null @@ -1,368 +0,0 @@ -@charset "UTF-8"; -:is(.admonition) { - display: flow-root; - margin: 1.5625em 0; - padding: 0 1.2rem; - color: var(--fg); - page-break-inside: avoid; - background-color: var(--bg); - border: 0 solid black; - border-inline-start-width: 0.4rem; - border-radius: 0.2rem; - box-shadow: - 0 0.2rem 1rem rgba(0, 0, 0, 0.05), - 0 0 0.1rem rgba(0, 0, 0, 0.1); -} -@media print { - :is(.admonition) { - box-shadow: none; - } -} -:is(.admonition) > * { - box-sizing: border-box; -} -:is(.admonition) :is(.admonition) { - margin-top: 1em; - margin-bottom: 1em; -} -:is(.admonition) > .tabbed-set:only-child { - margin-top: 0; -} -html :is(.admonition) > :last-child { - margin-bottom: 1.2rem; -} - -a.admonition-anchor-link { - display: none; - position: absolute; - left: -1.2rem; - padding-right: 1rem; -} -a.admonition-anchor-link:link, -a.admonition-anchor-link:visited { - color: var(--fg); -} -a.admonition-anchor-link:link:hover, -a.admonition-anchor-link:visited:hover { - text-decoration: none; -} -a.admonition-anchor-link::before { - content: '§'; -} - -:is(.admonition-title, summary.admonition-title) { - position: relative; - min-height: 4rem; - margin-block: 0; - margin-inline: -1.6rem -1.2rem; - padding-block: 0.8rem; - padding-inline: 4.4rem 1.2rem; - font-weight: 700; - background-color: rgba(68, 138, 255, 0.1); - print-color-adjust: exact; - -webkit-print-color-adjust: exact; - display: flex; -} -:is(.admonition-title, summary.admonition-title) p { - margin: 0; -} -html :is(.admonition-title, summary.admonition-title):last-child { - margin-bottom: 0; -} -:is(.admonition-title, summary.admonition-title)::before { - position: absolute; - top: 0.625em; - inset-inline-start: 1.6rem; - width: 2rem; - height: 2rem; - background-color: #448aff; - print-color-adjust: exact; - -webkit-print-color-adjust: exact; - mask-image: url('data:image/svg+xml;charset=utf-8,'); - -webkit-mask-image: url('data:image/svg+xml;charset=utf-8,'); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-size: contain; - content: ''; -} -:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link { - display: initial; -} - -details.admonition > summary.admonition-title::after { - position: absolute; - top: 0.625em; - inset-inline-end: 1.6rem; - height: 2rem; - width: 2rem; - background-color: currentcolor; - mask-image: var(--md-details-icon); - -webkit-mask-image: var(--md-details-icon); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-size: contain; - content: ''; - transform: rotate(0deg); - transition: transform 0.25s; -} -details[open].admonition > summary.admonition-title::after { - transform: rotate(90deg); -} -summary.admonition-title::-webkit-details-marker { - display: none; -} - -:root { - --md-details-icon: url("data:image/svg+xml;charset=utf-8,"); -} - -:root { - --md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,"); - --md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,"); -} - -:is(.admonition):is(.admonish-note) { - border-color: #448aff; -} - -:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(68, 138, 255, 0.1); -} -:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #448aff; - mask-image: var(--md-admonition-icon--admonish-note); - -webkit-mask-image: var(--md-admonition-icon--admonish-note); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) { - border-color: #00b0ff; -} - -:is(.admonish-abstract, .admonish-summary, .admonish-tldr) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(0, 176, 255, 0.1); -} -:is(.admonish-abstract, .admonish-summary, .admonish-tldr) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #00b0ff; - mask-image: var(--md-admonition-icon--admonish-abstract); - -webkit-mask-image: var(--md-admonition-icon--admonish-abstract); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-info, .admonish-todo) { - border-color: #00b8d4; -} - -:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(0, 184, 212, 0.1); -} -:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #00b8d4; - mask-image: var(--md-admonition-icon--admonish-info); - -webkit-mask-image: var(--md-admonition-icon--admonish-info); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) { - border-color: #00bfa5; -} - -:is(.admonish-tip, .admonish-hint, .admonish-important) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(0, 191, 165, 0.1); -} -:is(.admonish-tip, .admonish-hint, .admonish-important) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #00bfa5; - mask-image: var(--md-admonition-icon--admonish-tip); - -webkit-mask-image: var(--md-admonition-icon--admonish-tip); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) { - border-color: #00c853; -} - -:is(.admonish-success, .admonish-check, .admonish-done) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(0, 200, 83, 0.1); -} -:is(.admonish-success, .admonish-check, .admonish-done) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #00c853; - mask-image: var(--md-admonition-icon--admonish-success); - -webkit-mask-image: var(--md-admonition-icon--admonish-success); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) { - border-color: #64dd17; -} - -:is(.admonish-question, .admonish-help, .admonish-faq) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(100, 221, 23, 0.1); -} -:is(.admonish-question, .admonish-help, .admonish-faq) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #64dd17; - mask-image: var(--md-admonition-icon--admonish-question); - -webkit-mask-image: var(--md-admonition-icon--admonish-question); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) { - border-color: #ff9100; -} - -:is(.admonish-warning, .admonish-caution, .admonish-attention) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(255, 145, 0, 0.1); -} -:is(.admonish-warning, .admonish-caution, .admonish-attention) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #ff9100; - mask-image: var(--md-admonition-icon--admonish-warning); - -webkit-mask-image: var(--md-admonition-icon--admonish-warning); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) { - border-color: #ff5252; -} - -:is(.admonish-failure, .admonish-fail, .admonish-missing) - > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(255, 82, 82, 0.1); -} -:is(.admonish-failure, .admonish-fail, .admonish-missing) - > :is(.admonition-title, summary.admonition-title)::before { - background-color: #ff5252; - mask-image: var(--md-admonition-icon--admonish-failure); - -webkit-mask-image: var(--md-admonition-icon--admonish-failure); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-danger, .admonish-error) { - border-color: #ff1744; -} - -:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(255, 23, 68, 0.1); -} -:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #ff1744; - mask-image: var(--md-admonition-icon--admonish-danger); - -webkit-mask-image: var(--md-admonition-icon--admonish-danger); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-bug) { - border-color: #f50057; -} - -:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(245, 0, 87, 0.1); -} -:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #f50057; - mask-image: var(--md-admonition-icon--admonish-bug); - -webkit-mask-image: var(--md-admonition-icon--admonish-bug); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-example) { - border-color: #7c4dff; -} - -:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(124, 77, 255, 0.1); -} -:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #7c4dff; - mask-image: var(--md-admonition-icon--admonish-example); - -webkit-mask-image: var(--md-admonition-icon--admonish-example); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -:is(.admonition):is(.admonish-quote, .admonish-cite) { - border-color: #9e9e9e; -} - -:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) { - background-color: rgba(158, 158, 158, 0.1); -} -:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before { - background-color: #9e9e9e; - mask-image: var(--md-admonition-icon--admonish-quote); - -webkit-mask-image: var(--md-admonition-icon--admonish-quote); - mask-repeat: no-repeat; - -webkit-mask-repeat: no-repeat; - mask-size: contain; - -webkit-mask-repeat: no-repeat; -} - -.navy :is(.admonition) { - background-color: var(--sidebar-bg); -} - -.ayu :is(.admonition), -.coal :is(.admonition) { - background-color: var(--theme-hover); -} - -.rust :is(.admonition) { - background-color: var(--sidebar-bg); - color: var(--sidebar-fg); -} -.rust .admonition-anchor-link:link, -.rust .admonition-anchor-link:visited { - color: var(--sidebar-fg); -} diff --git a/docs/overview/adapters.mdx b/docs/overview/adapters.mdx new file mode 100644 index 0000000..b808ef1 --- /dev/null +++ b/docs/overview/adapters.mdx @@ -0,0 +1,116 @@ +--- +title: Adapters (viem & ethers) +description: Learn how the SDK extends your existing viem or ethers setup with ZKsync-specific functionality. +--- + +# Adapters: `viem` & `ethers` + +The SDK is designed to work _with_ the tools you already know and love. +It’s not a standalone library — it’s an **extension** that plugs directly into your existing [`viem`](https://viem.sh) or [`ethers.js`](https://docs.ethers.io/) setup. + + + This design means you can keep your existing provider, signer, and connection logic. The SDK + simply layers ZKsync-specific actions on top. + + +## Why an Adapter Model? + +- **Bring Your Own Stack:** Integrates directly with `viem` (`PublicClient`, `WalletClient`) or `ethers` providers and signers. +- **Familiar Developer Experience:** Keep your existing connection and signing flow unchanged. +- **Lightweight & Focused:** Only adds ZKsync-specific functionality like deposits, withdrawals, and soon interop — nothing more. + +## Installation + +```bash +# For viem users +npm install @dutterbutter/zksync-sdk viem + +# For ethers.js users +npm install @dutterbutter/zksync-sdk ethers +``` + +You install the **core SDK** plus the adapter matching your stack. + +## How to Use + +The SDK extends your existing client. Configure **viem** or **ethers** normally, then wrap them with the SDK’s adapter factory. + + +```ts viem.ts +import { createPublicClient, http, createWalletClient, parseEther } from "viem"; +import { createViemSdk, createViemClient } from "@dutterbutter/zksync-sdk/viem"; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +const l1 = createPublicClient({ transport: http(L1_RPC) }); +const l2 = createPublicClient({ transport: http(L2_RPC) }); + +const l1Wallet: WalletClient = createWalletClient({ +account, +transport: http(L1_RPC), +}); + +const client = createViemClient({ l1, l2, l1Wallet }); +const sdk = createViemSdk(client); + +const me = account.address as Address; +const params = { +amount: parseEther('0.01'), +token: ETH_ADDRESS, +to: me, +// optional: +// l2GasLimit: 300_000n, +// gasPerPubdata: 800n, +// operatorTip: 0n, +// refundRecipient: me, +} as const; + +const handle = await sdk.deposits.create({ params }); +await sdk.deposits.wait(handle, { for: 'l1' }); // L1 included +await sdk.deposits.wait(handle, { for: 'l2' }); // L2 executed + +console.log('Deposit complete ✅'); + +```` + +```ts ethers.ts +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; +import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + +const L1_RPC = new JsonRpcProvider('https://sepolia.infura.io/v3/...'); +const L2_RPC = new JsonRpcProvider('https://zksync-testnet.rpc'); +const signer = new Wallet(process.env.PRIVATE_KEY!, L1_RPC); + +const client = await createEthersClient({ l1Provider: L1_RPC, l2Provider: L2_RPC, signer }); +const sdk = createEthersSdk(client); + +const params = { + amount: parseEther('0.01'), + token: ETH_ADDRESS, + to: signer.address, + // optional: + // l2GasLimit: 300_000n, + // gasPerPubdata: 800n, + // operatorTip: 0n, + // refundRecipient: me, +} as const; + +const handle = await sdk.deposits.create({ params }); +await sdk.deposits.wait(handle, { for: 'l1' }); // L1 included +await sdk.deposits.wait(handle, { for: 'l2' }); // L2 executed + +console.log('Deposit complete ✅'); +```` + + + +## Key Principles + +- **No Key Management:** The SDK never asks for or stores private keys — signing stays with your `WalletClient` or `Signer`. +- **API Parity:** Both adapters expose the same API. Calling `sdk.deposits.quote()` works identically with `viem` or `ethers`. +- **Easy Migration:** Switch between `ethers` and `viem` by only changing the initialization code. + + diff --git a/docs/overview/finalization.mdx b/docs/overview/finalization.mdx new file mode 100644 index 0000000..35136be --- /dev/null +++ b/docs/overview/finalization.mdx @@ -0,0 +1,114 @@ +--- +title: Finalization (Withdrawals) +description: Withdrawals from ZKsync (L2) only complete on Ethereum (L1) after you explicitly call `finalize`. +--- + +When withdrawing from ZKsync (L2) back to Ethereum (L1), **funds are _not_ automatically released on L1** after your L2 tx is included. + +Withdrawals are a **two-step process**: + +1. **Initiate on L2** — call `withdraw()` (via the SDK’s `create`) to start the withdrawal. + This burns/locks funds on L2 and emits logs; **funds are still unavailable on L1**. +2. **Finalize on L1** — call **`finalize(l2TxHash)`** to release funds on L1. + This submits an L1 tx; only then does your ETH or token balance increase on Ethereum. + + + If you **never finalize**, your funds remain locked: visible as “ready to withdraw,” but + unavailable on L1. Anyone can finalize on your behalf, but you typically do it. + + +## Why finalization matters + +- **Funds remain locked** until you (or anyone) finalizes. +- **Anyone can finalize** — typically the withdrawer does. +- **Finalization costs L1 gas** — budget for it. + +## Finalization methods + +| Method | Purpose | Returns | +| ------------------------------------------ | ----------------------------------------------------------- | --------------------- | +| `withdrawals.status(h \| l2TxHash)` | Snapshot phase (`UNKNOWN` → `FINALIZED`) | `WithdrawalStatus` | +| `withdrawals.wait(h \| l2TxHash, { for })` | Block until a checkpoint (`'l2' \| 'ready' \| 'finalized'`) | Receipt or `null` | +| `withdrawals.finalize(l2TxHash)` | **Send** the L1 finalize tx | `{ status, receipt }` | + + + All methods accept either a **handle** (from `create`) or a **raw L2 tx hash**. If you only have + the hash, you can still finalize. + + +## Phases + +| Phase | Meaning | +| ------------------- | ------------------------------------------------- | +| `UNKNOWN` | Handle doesn’t contain an L2 hash yet. | +| `L2_PENDING` | L2 tx not yet included. | +| `PENDING` | L2 included, but not yet ready to finalize on L1. | +| `READY_TO_FINALIZE` | Finalization on L1 would succeed now. | +| `FINALIZED` | Finalized on L1; funds released. | + +## Examples + + +```ts finalize-by-handle.ts theme={null} +// 1) Create on L2 +const withdrawal = await sdk.withdrawals.create({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: myAddress, +}); + +// 2) Wait until finalizable (no side effects) +await sdk.withdrawals.wait(withdrawal, { for: 'ready', pollMs: 5500 }); + +// 3) Finalize on L1 +const { status, receipt } = await sdk.withdrawals.finalize(withdrawal.l2TxHash); + +console.log(status.phase); // "FINALIZED" +console.log(receipt?.transactionHash); // L1 finalize tx hash + +```` + +```ts finalize-by-hash.ts theme={null} +// If you only have the L2 tx hash: +const l2TxHash = '0x...'; + +// Optionally confirm readiness first +const s = await sdk.withdrawals.status(l2TxHash); +if (s.phase !== 'READY_TO_FINALIZE') { + await sdk.withdrawals.wait(l2TxHash, { for: 'ready', timeoutMs: 30 * 60_000 }); +} + +// Then finalize +const { status, receipt } = await sdk.withdrawals.finalize(l2TxHash); +```` + + + +Prefer the **no-throw** variants in UIs/services that want explicit flow control: + +```ts try-finalize.ts theme={null} +const r = await sdk.withdrawals.tryFinalize(l2TxHash); +if (!r.ok) { + // Show a toast / retry UI + console.error('Finalize failed:', r.error); +} else { + console.log('Finalized on L1:', r.value.receipt?.transactionHash); +} +``` + +## Operational tips + +- **Gate UX with phases:** Display a **Finalize** button only when `status.phase === 'READY_TO_FINALIZE'`. +- **Polling cadence:** `wait(..., { for: 'ready' })` defaults to ~**5500 ms**. Adjust with `pollMs` if needed. +- **Timeouts:** Use `timeoutMs` for long windows and fall back to `status(...)` to keep the UI responsive. +- **Receipts can be `null`:** `wait(..., { for: 'finalized' })` can resolve `null` if finalized but receipt isn’t retrievable; consider showing a link to the L1 explorer based on the tx hash you submitted. + +## Common errors + +- **RPC/network hiccups:** thrown `ZKsyncError` with kind **`RPC`**. Retry with backoff. +- **Internal decode issues:** thrown `ZKsyncError` with kind **`INTERNAL`**. Capture logs and report. + +## See also + +- [Status vs Wait](/overview/status-vs-wait) +- [Withdrawals guide](/guides/withdrawals) diff --git a/docs/overview/index.mdx b/docs/overview/index.mdx new file mode 100644 index 0000000..6304f00 --- /dev/null +++ b/docs/overview/index.mdx @@ -0,0 +1,50 @@ +--- +title: Welcome +description: Learn what the `@zksync-sdk` is and how it simplifies ZKsync cross-chain flows for viem and ethers. +--- + +# Introduction + +The **`@zksync-sdk`** is a lightweight extension for [`viem`](https://viem.sh) and [`ethers`](https://docs.ethers.io/) that makes ZKsync cross-chain actions simple and consistent. + +Instead of re-implementing accounts or low-level RPC logic, this SDK focuses only on **ZKsync-specific flows**: + +- Deposits (L1 → L2) +- Withdrawals (L2 → L1, including finalization) +- Try variants for functional error handling (e.g. `tryCreate`) +- Status & wait helpers +- ZKsync-specific JSON-RPC methods + + + The SDK doesn’t replace your existing Ethereum libraries — it **extends** them with ZKsync-only + capabilities while keeping your current tooling intact. + + +### Key Supported Features + +- **Deposits (L1 → L2)** — ETH, Custom Base Token and ERC-20 + - **Initiate on L1:** build and send the deposit transaction from Ethereum. + - **Track progress:** query intermediate states (queued, included, executed). + - **Verify completion on L2:** confirm funds credited/available on ZKsync. + +- **Withdrawals (L2 → L1)** — ETH, Custom Base Token and ERC-20 + - **Initiate on L2:** create the withdrawal transaction on ZKsync. + - **Track progress:** monitor execution and finalization availability. + - **Finalize on L1:** finalize withdrawal to release funds back to Ethereum. + +- **ZKsync RPC** + - **`getBridgehubAddress`** (`zks_getBridgehubContract`) – resolve the canonical Bridgehub contract address. + - **`getL2ToL1LogProof`** (`zks_getL2ToL1LogProof`) – retrieve the log proof for an L2 → L1 transaction. + - **`getReceiptWithL2ToL1`** _(receipt extension)_ – returns a standard Ethereum `TransactionReceipt` **augmented** with `l2ToL1Logs`. + +## What you’ll find here + +- [**Mental model**](./overview/mental-model) — how to think about the core methods (`quote → prepare → create → status → wait → finalize`). +- [**Adapters (viem & ethers)**](./overview/adapters) — how the SDK integrates with your existing stack. +- [**Withdrawal Finalization**](./overview/finalization) — understand the finalization process for withdrawals and how to ensure they are completed. + +## Next steps + +👉 Ready to build? Jump to the [Quickstart](../quickstart/index). + + diff --git a/docs/overview/mental-model.mdx b/docs/overview/mental-model.mdx new file mode 100644 index 0000000..46d0fef --- /dev/null +++ b/docs/overview/mental-model.mdx @@ -0,0 +1,155 @@ +--- +title: Mental model +description: Understand the predictable lifecycle (`quote → prepare → create → status → wait → finalize`) that powers every L1–L2 and L2–L1 action in the SDK. +--- + +# Mental Model + +The SDK is designed around a **predictable, layered API** for handling L1 → L2 and L2 → L1 operations. +Every action — whether a deposit or a withdrawal — follows a consistent lifecycle. +Understanding this lifecycle is key to using the SDK effectively. + +```bash +quote → prepare → create → status → wait → (finalize*) +``` + +- The first five steps are common to both **Deposits** and **Withdrawals**. +- Withdrawals require an additional **`finalize`** step to prove and claim funds on L1. + + + You can enter this lifecycle at **different stages** depending on how much control you need. + + +## The Core API: A Layered Approach + +The core methods give you **progressively more automation**. +Start by just gathering info (`quote`), move to building transactions yourself (`prepare`), or execute the entire flow with a single call (`create`). + +### `quote(params)` + +> _"What will this operation involve and cost?"_ + +- **Read-only dry run**: performs no transactions and has no side effects. +- Returns a `Quote` object with estimated fees, gas costs, and the planned steps. + + + Best for **displaying a confirmation screen** or cost estimate to a user before committing. + + +### `prepare(params)` + +> _"Build the transactions for me, but let me send them."_ + +- Constructs all necessary transactions as an array of `TransactionRequest` objects inside a `Plan`. +- Does **not** sign or send them. + + + Best for **custom workflows** (e.g., multisigs or when you want to review the raw tx data before + signing). + + +### `create(params)` + +> _"Prepare, sign, and send in one go."_ + +- Calls `prepare` internally, then signs & dispatches the txs using your configured signer. +- Returns a `Handle` — a lightweight tracker for later `status` or `wait`. + + + Best for **standard use cases** where you just want to kick off a deposit or withdrawal. + + +### `status(handle | txHash)` + +> _"Where is my transaction right now?"_ + +- Non-blocking: returns the **current state** of an operation. +- Works with a `Handle` or raw transaction hash. + +Example return values: + +```ts +// Deposits +{ + phase: 'L1_PENDING' | 'L2_EXECUTED'; +} + +// Withdrawals +{ + phase: 'L1_INCLUDED' | 'L2_PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED'; +} +``` + +Best for **polling in a UI** to show live progress without blocking. + +### `wait(handle, { for })` + +> _"Pause until a specific checkpoint is reached."_ + +- Blocking async method: polls until the desired checkpoint, then resolves with the tx receipt. + +Common checkpoints: + +- **Deposits:** `'l1'` (included on L1) or `'l2'` (executed on L2). +- **Withdrawals:** `'l2'` (included), `'ready'` (ready to finalize), `'finalized'` (L1 finalization done). + + + Best for **scripts or backends** that need to ensure one step is complete before the next. + + +### `finalize(l2TxHash)` _(Withdrawals Only)_ + +> _"My funds are ready on L1. Finalize and release them."_ + +- Executes the **final step** of a withdrawal after `status` reports `READY_TO_FINALIZE`. + +Best for **claiming funds back to Ethereum** once the withdrawal is ready. + +## Error Handling: The `try*` Philosophy + +For functional-style error handling (no `try/catch`), every core method has a **`try*` variant** (`tryQuote`, `tryCreate`, etc.). + +```ts +// Imperative style +try { + const handle = await sdk.withdrawals.create(params); + // happy path +} catch (error) { + // sad path +} + +// Functional style +const result = await sdk.withdrawals.tryCreate(params); + +if (result.ok) { + const handle = result.value; +} else { + console.error('Withdrawal failed:', result.error); +} +``` + + + Best for **applications that want explicit, predictable error handling** and to avoid uncaught + exceptions. + + +## Putting It All Together + +You can compose flows as simple or complex as needed. + +### Simple Flow + +```ts +// 1. Create the deposit +const depositHandle = await sdk.deposits.create(params); + +// 2. Wait for it to be finalized on L2 +const receipt = await sdk.deposits.wait(depositHandle, { for: 'l2' }); + +console.log('Deposit complete!'); +``` + + diff --git a/docs/overview/status-vs-wait.mdx b/docs/overview/status-vs-wait.mdx new file mode 100644 index 0000000..6eaf638 --- /dev/null +++ b/docs/overview/status-vs-wait.mdx @@ -0,0 +1,159 @@ +--- +title: Status vs Wait +description: Snapshot progress with `status(...)` or block until a checkpoint with `wait(..., { for })` for deposits and withdrawals. +--- + +The SDK exposes two complementary ways to track progress: + +- **`status(...)`** — returns a **non-blocking snapshot** of where an operation is. +- **`wait(..., { for })`** — **blocks/polls** until a specified checkpoint is reached. + +Both apply to **deposits** and **withdrawals**. Use `status(...)` for UI refreshes; use `wait(...)` when you need to gate logic on inclusion/finality. + + + You can pass **either** a handle returned from `create(...)` **or** a raw transaction hash. + + +## Withdrawals + +### `withdrawals.status(h | l2TxHash): Promise` + +**Input** + +- `h`: a `WithdrawalWaitable` (e.g., from `create`) **or** the L2 tx hash `Hex`. + +**Phases** +| Phase | Meaning | +| -------------------- | ----------------------------------------------------------- | +| `UNKNOWN` | Handle doesn’t contain an L2 hash yet. | +| `L2_PENDING` | L2 tx not yet included. | +| `PENDING` | L2 included, **not** yet ready to finalize on L1. | +| `READY_TO_FINALIZE` | Finalization on L1 would succeed now. | +| `FINALIZED` | Finalized on L1; funds released. | + +**Notes** + +- No L2 receipt ⇒ `L2_PENDING`. +- Finalization key derivable but not ready ⇒ `PENDING`. +- Already finalized ⇒ `FINALIZED`. + +```ts withdrawals-status.ts theme={null} +const s = await sdk.withdrawals.status(handleOrHash); +// s.phase ∈ 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +``` + +### `withdrawals.wait(h | l2TxHash, { for, pollMs?, timeoutMs? })` + +**Targets** + +| Target | Resolves with | +| ---------------------- | --------------------------------------------------------------------------- | +| `{ for: 'l2' }` | **L2 receipt** (`TransactionReceipt \| null`) | +| `{ for: 'ready' }` | **`null`** when finalization becomes possible | +| `{ for: 'finalized' }` | **L1 receipt** when finalized, or `null` if finalized but receipt not found | + +**Behavior** + +- If the handle has **no L2 hash**, returns `null` immediately. +- Default polling: **5500 ms** (override via `pollMs`). +- `timeoutMs` returns `null` on deadline. + +```ts withdrawals-wait.ts theme={null} +// Wait for L2 inclusion → get L2 receipt (augmented with l2ToL1Logs if available) +const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2', pollMs: 5000 }); + +// Wait until it becomes finalizable (no side effects) +await sdk.withdrawals.wait(handle, { for: 'ready' }); + +// Wait for L1 finalization → L1 receipt (or null if not retrievable) +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', timeoutMs: 15 * 60_000 }); +``` + + + Building a UI? Use `status(...)` to paint current phase and enable/disable the **Finalize** button + when phase is `READY_TO_FINALIZE`. + + +## Deposits + +### `deposits.status(h | l1TxHash): Promise` + +**Input** + +- `h`: `DepositWaitable` (from `create`) **or** L1 tx hash `Hex`. + +**Phases** + +| Phase | Meaning | +| ------------- | ------------------------------------------------- | +| `UNKNOWN` | No L1 hash present on the handle. | +| `L1_PENDING` | L1 receipt missing. | +| `L1_INCLUDED` | L1 included; L2 hash not yet derivable from logs. | +| `L2_PENDING` | L2 hash known but L2 receipt missing. | +| `L2_EXECUTED` | L2 receipt present with `status === 1`. | +| `L2_FAILED` | L2 receipt present with `status !== 1`. | + +```ts deposits-status.ts theme={null} +const s = await sdk.deposits.status(handleOrL1Hash); +// s.phase ∈ 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' +``` + +### `deposits.wait(h | l1TxHash, { for: 'l1' | 'l2' })` + +**Targets** + +| Target | Resolves with | +| --------------- | ------------------------------------------------------------------- | +| `{ for: 'l1' }` | **L1 receipt** or `null` | +| `{ for: 'l2' }` | **L2 receipt** or `null` (waits L1 inclusion **then** L2 execution) | + +```ts deposits-wait.ts theme={null} +const l1Rcpt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Rcpt = await sdk.deposits.wait(handle, { for: 'l2' }); +``` + + + `wait(..., { for: 'l2' })` waits for both **L1 inclusion** and **canonical L2 execution**. + + +## Practical patterns + +### Pick the right tool + +- **Use `status(...)`** for **poll-less UI refreshes** (e.g., on page focus or interval timers you control). +- **Use `wait(...)`** for **workflow gating** (scripts, jobs, “continue when X happens”). + +### Timeouts & polling + +```ts polling.ts theme={null} +const ready = await sdk.withdrawals.wait(handle, { + for: 'ready', + pollMs: 5500, // minimum enforced internally + timeoutMs: 30 * 60_000, // 30 minutes; returns null on deadline +}); +if (ready === null) { + // timeout or not yet finalizable — decide whether to retry or show a hint +} +``` + +### Error handling + +- Network hiccup while fetching receipts ⇒ throws `ZKsyncError` of kind **`RPC`**. +- Internal decode issue ⇒ throws `ZKsyncError` of kind **`INTERNAL`**. + +Prefer no-throw variants if you want explicit flow control: + +```ts no-throw.ts theme={null} +const r = await sdk.withdrawals.tryWait(handle, { for: 'finalized' }); +if (!r.ok) { + console.error('Finalize wait failed:', r.error); +} else { + console.log('Finalized L1 receipt:', r.value); +} +``` + +## Tips & edge cases + +- **Handles vs hashes:** Passing a handle without the relevant hash yields `UNKNOWN`/`null`. If you already have a hash, pass the hash directly. +- **Finalization windows:** For withdrawals, `READY_TO_FINALIZE` can take a while. Use `status(...)` to keep the UI responsive and `wait(..., { for: 'finalized' })` only where blocking makes sense. +- **Retries:** If a wait returns `null` due to `timeoutMs`, you can safely call `status(...)` to decide whether to keep waiting or surface guidance to the user. diff --git a/docs/quickstart/deposits.mdx b/docs/quickstart/deposits.mdx new file mode 100644 index 0000000..966fa22 --- /dev/null +++ b/docs/quickstart/deposits.mdx @@ -0,0 +1,202 @@ +--- +title: ETH Deposit (L1 → L2) +description: Get your first ETH deposit from Ethereum (L1) to ZKsync (L2). +--- + +## 1. Prerequisites + +- You have [Bun](https://bun.sh/) 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: + + + ```bash title="viem" + bun install @dutterbutter/zksync-sdk viem dotenv + ``` + + ```bash title="ethers" + bun install @dutterbutter/zksync-sdk ethers dotenv + ``` + + + +Create a `.env` file in your project root: + +```env +# 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 + + + ```ts title="deposit/viem.ts" + 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); + }); + ``` + + ```tsx title="deposit/ethers.ts" + 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 using `bun`. + + + ``` bash title="viem" + bun run deposit/viem.ts + ``` + + ```bash title="ethers" + bun run deposit/ethers.ts + ``` + + + +You’ll see logs for the L1 transaction, then L2 execution, followed by updated balances. + +## 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. diff --git a/docs/quickstart/index.mdx b/docs/quickstart/index.mdx new file mode 100644 index 0000000..4de65db --- /dev/null +++ b/docs/quickstart/index.mdx @@ -0,0 +1,42 @@ +--- +title: Adapter Choice +description: Get your first ZKsync deposit done using the @zksync-sdk. +--- + +# Choosing Your Adapter: `viem` vs. `ethers` + +The SDK is designed to work with both `viem` and `ethers.js`, the two most popular Ethereum libraries. Since the SDK offers **identical functionality** for both, the choice comes down to your project's needs and your personal preference. + +## The Short Answer (TL;DR) + +- **If you're adding the SDK to an existing project:** Use the adapter for the library you're already using. +- **If you're starting a new project:** The choice is yours. `viem` is generally recommended for new projects due to its modern design, smaller bundle size, and excellent TypeScript support. + +You can't make a wrong choice. Both adapters are fully supported and provide the same features. + + + + L1 → L2 deposit — submit a deposit, and monitor until confirmed. + + + + L2 → L1 withdrawal — submit a withdrawal, and monitor until readied. + + + +## What you’ll do + +Each Quickstart walks you through how to: + +1. **Install** the adapter package. +2. **Configure** a client or signer. +3. **Run** a deposit (L1 → L2) or withdrawal (L2 → L1) as a working example. +4. **Track** the status until it’s complete. diff --git a/docs/quickstart/withdrawals.mdx b/docs/quickstart/withdrawals.mdx new file mode 100644 index 0000000..fd1dd2c --- /dev/null +++ b/docs/quickstart/withdrawals.mdx @@ -0,0 +1,201 @@ +--- +title: ETH Withdrawal (L2 → L1) +description: Get your first ETH withdrawal from ZKsync (L2) to Ethereum (L1). +--- + +## 1. Prerequisites + +- You have [Bun](https://bun.sh/) installed. +- A funded **L1 wallet** with ETH for both the withdrawal amount and L1 gas fees + +Use a test network like **Sepolia** for experimentation. + +## 2. Installation + +Choose your adapter and install the SDK + adapter package: + + + ```bash title="viem" + bun install @dutterbutter/zksync-sdk viem dotenv + ``` + + ```bash title="ethers" + bun install @dutterbutter/zksync-sdk ethers dotenv + ``` + + + +Create a `.env` file in your project root: + +```env +# 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 withdrawal script + + + ```ts title="withdrawals/viem.ts" + import { + createPublicClient, + createWalletClient, + http, + parseEther, + type Account, + type Chain, + type Transport, + type WalletClient, + } from 'viem'; + import { privateKeyToAccount } from 'viem/accounts'; + + import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; + import type { Address } from '@dutterbutter/zksync-sdk/core'; + import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + + const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX + const L2_RPC = 'http://localhost:3050'; // your L2 RPC + const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; + + async function main() { + if (!PRIVATE_KEY) throw new Error('Set your PRIVATE_KEY in the environment'); + + // Clients + const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); + const l1 = createPublicClient({ transport: http(L1_RPC) }); + const l2 = createPublicClient({ transport: http(L2_RPC) }); + const l1Wallet: WalletClient = createWalletClient({ + account, + transport: http(L1_RPC), + }); + // Need to provide an L2 wallet client for sending L2 withdraw tx + const l2Wallet = createWalletClient({ + account, + transport: http(L2_RPC), + }); + + const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); + const sdk = createViemSdk(client); + + const me = account.address as Address; + + const params = { + token: ETH_ADDRESS, // ETH Address + amount: parseEther('0.01'), + to: me, + } as const; + + const quote = await sdk.withdrawals.quote(params); + console.log('QUOTE:', quote); + + const prepared = await sdk.withdrawals.prepare(params); + console.log('PREPARE:', prepared); + + const created = await sdk.withdrawals.create(params); + console.log('CREATE:', created); + + console.log('STATUS (initial):', await sdk.withdrawals.status(created)); + + const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); + console.log('L2 included tx:', l2Receipt?.transactionHash); + + await sdk.withdrawals.wait(created, { for: 'ready' }); + console.log('STATUS (ready):', await sdk.withdrawals.status(created)); + + const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); + console.log('TRY FINALIZE:', fin); + + const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); + console.log('Finalized. L1 receipt:', l1Receipt?.transactionHash); + } + + main().catch((e) => { + console.error(e); + process.exit(1); + }); + ``` + + ```tsx title="withdrawals/ethers.ts" + import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; + import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; + import type { Address } from '@dutterbutter/zksync-sdk/core'; + import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; + + const L1_RPC = 'http://localhost:8545'; + const L2_RPC = 'http://localhost:3050'; + const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; + + async function main() { + if (!PRIVATE_KEY) throw new Error('Set your PRIVATE_KEY in the environment'); + + const l1 = new JsonRpcProvider(L1_RPC); + const l2 = new JsonRpcProvider(L2_RPC); + const signer = new Wallet(PRIVATE_KEY, l1); + + const client = await createEthersClient({ l1, l2, signer }); + const sdk = createEthersSdk(client); + + const me = (await signer.getAddress()) as Address; + + const params = { + token: ETH_ADDRESS, // ETH token on this chain + amount: parseEther('1'), + to: me, + // l2GasLimit?: 300_000n, fee overrides, etc... + } as const; + + const quote = await sdk.withdrawals.quote(params); + console.log('QUOTE:', quote); + + const prepared = await sdk.withdrawals.prepare(params); + console.log('PREPARE:', prepared); + + const created = await sdk.withdrawals.create(params); + console.log('CREATE:', created); + + console.log('STATUS (initial):', await sdk.withdrawals.status(created)); + + // Wait for L2 inclusion + const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); + console.log('L2 included:', l2Receipt?.hash); + + // Wait until ready to finalize + await sdk.withdrawals.wait(created, { for: 'ready' }); + console.log('STATUS (ready):', await sdk.withdrawals.status(created)); + + // Try to finalize (no-op if already finalized by someone else) + const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); + console.log('TRY FINALIZE:', fin); + + // Wait for finalization + const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); + console.log('Finalized. L1 receipt:', l1Receipt?.hash); + } + + main().catch((e) => { + console.error(e); + process.exit(1); + }); + ``` + + + +## 4. Run it + + + ``` bash title="viem" bun run withdrawals/viem.ts ``` ```bash title="ethers" bun run + withdrawals/ethers.ts ``` + + +You’ll see logs for the L2 transaction, then L1 finalization readiness, L1 finalization execution, followed by updated balances. + +## 5. Troubleshooting + +- **Insufficient funds on L2:** Ensure enough ETH for the withdrawal **and** L1 gas. +- **Invalid PRIVATE_KEY:** Must be 0x + 64 hex chars. +- **Stuck at wait(..., `{ for: 'l1' }`):** Verify L2_RPC_URL and network health; check sdk.deposits.status(handle) to see the current phase. diff --git a/docs/snippets/ethers/deposit-erc20.ts b/docs/snippets/ethers/deposit-erc20.ts deleted file mode 100644 index effabc7..0000000 --- a/docs/snippets/ethers/deposit-erc20.ts +++ /dev/null @@ -1,63 +0,0 @@ -// examples/deposit-erc20.ts -import { JsonRpcProvider, Wallet, parseUnits, type Signer } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - const l1 = new JsonRpcProvider(L1_RPC); - const l2 = new JsonRpcProvider(L2_RPC); - const signer = new Wallet(PRIVATE_KEY, l1); - - const client = await createEthersClient({ l1, l2, signer }); - const sdk = createEthersSdk(client); - - // ERC20 on sepolia - // Deploy your own if you can not use this one - const TOKEN = '0x42E331a2613Fd3a5bc18b47AE3F01e1537fD8873'; - - const me = (await signer.getAddress()); - const depositAmount = parseUnits('250', 18); - - // quote - const quote = await sdk.deposits.quote({ token: TOKEN, to: me, amount: depositAmount }); - console.log('QUOTE:', quote); - - const prepare = await sdk.deposits.prepare({ token: TOKEN, to: me, amount: depositAmount }); - console.log('PREPARE:', prepare); - - const create = await sdk.deposits.create({ token: TOKEN, to: me, amount: depositAmount }); - console.log('CREATE:', create); - - const status = await sdk.deposits.status(create); - console.log('STATUS (immediate):', status); - - // Wait until the L1 tx is included - const receipt = await sdk.deposits.wait(create, { for: 'l1' }); - console.log( - 'Included at block:', - receipt?.blockNumber, - 'status:', - receipt?.status, - 'hash:', - receipt?.hash, - ); - - // Wait until the L2 tx is included - const l2Receipt = await sdk.deposits.wait(create, { for: 'l2' }); - console.log( - 'Included at block:', - l2Receipt?.blockNumber, - 'status:', - l2Receipt?.status, - 'hash:', - l2Receipt?.hash, - ); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/ethers/deposit-eth.ts b/docs/snippets/ethers/deposit-eth.ts deleted file mode 100644 index 47067c6..0000000 --- a/docs/snippets/ethers/deposit-eth.ts +++ /dev/null @@ -1,82 +0,0 @@ -// examples/deposit-eth.ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - if (!PRIVATE_KEY) { - throw new Error('Set your PRIVATE_KEY in the .env file'); - } - const l1 = new JsonRpcProvider(L1_RPC); - const l2 = new JsonRpcProvider(L2_RPC); - const signer = new Wallet(PRIVATE_KEY, l1); - - const balance = await l1.getBalance(signer.address); - console.log('L1 balance:', balance.toString()); - - const balanceL2 = await l2.getBalance(signer.address); - console.log('L2 balance:', balanceL2.toString()); - - const client = await createEthersClient({ l1, l2, signer }); - const sdk = createEthersSdk(client); - - const me = (await signer.getAddress()); - const params = { - amount: parseEther('.01'), // 0.01 ETH - to: me, - token: ETH_ADDRESS, - // optional: - // l2GasLimit: 300_000n, - // gasPerPubdata: 800n, - // operatorTip: 0n, - // refundRecipient: me, - } as const; - - // Quote - const quote = await sdk.deposits.quote(params); - console.log('QUOTE response: ', quote); - - const prepare = await sdk.deposits.prepare(params); - console.log('PREPARE response: ', prepare); - - // Create (prepare + send) - const create = await sdk.deposits.create(params); - console.log('CREATE response: ', create); - - const status = await sdk.deposits.status(create); - console.log('STATUS response: ', status); - - // Wait (for now, L1 inclusion) - const receipt = await sdk.deposits.wait(create, { for: 'l1' }); - console.log( - 'Included at block:', - receipt?.blockNumber, - 'status:', - receipt?.status, - 'hash:', - receipt?.hash, - ); - - const status2 = await sdk.deposits.status(create); - console.log('STATUS2 response: ', status2); - - // Wait (for now, L2 inclusion) - const l2Receipt = await sdk.deposits.wait(create, { for: 'l2' }); - console.log( - 'Included at block:', - l2Receipt?.blockNumber, - 'status:', - l2Receipt?.status, - 'hash:', - l2Receipt?.hash, - ); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/ethers/withdrawals-erc20.ts b/docs/snippets/ethers/withdrawals-erc20.ts deleted file mode 100644 index 9a29ba8..0000000 --- a/docs/snippets/ethers/withdrawals-erc20.ts +++ /dev/null @@ -1,80 +0,0 @@ -// examples/withdrawals-erc20.ts -import { JsonRpcProvider, Wallet, parseUnits } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -// Replace with a real **L2 ERC-20 token address** you hold on L2 -const L1_ERC20_TOKEN = '0x42E331a2613Fd3a5bc18b47AE3F01e1537fD8873'; - -async function main() { - const l1 = new JsonRpcProvider(L1_RPC); - const l2 = new JsonRpcProvider(L2_RPC); - const signer = new Wallet(PRIVATE_KEY, l1); - - const client = createEthersClient({ l1, l2, signer }); - const sdk = createEthersSdk(client); - - const me = (await signer.getAddress()); - const l2Token = await sdk.helpers.l2TokenAddress(L1_ERC20_TOKEN); - - // Prepare withdraw params - const params = { - token: l2Token, // L2 ERC-20 - amount: parseUnits('25', 18), // withdraw 25 tokens - to: me, - // l2GasLimit: 300_000n, - } as const; - - // -------- Dry runs / planning -------- - console.log('TRY QUOTE:', await sdk.withdrawals.tryQuote(params)); - console.log('QUOTE:', await sdk.withdrawals.quote(params)); - console.log('TRY PREPARE:', await sdk.withdrawals.tryPrepare(params)); - console.log('PREPARE:', await sdk.withdrawals.prepare(params)); - - // -------- Create (L2 approvals if needed + withdraw) -------- - const created = await sdk.withdrawals.create(params); - console.log('CREATE:', created); - - // Wait for L2 inclusion - const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); - console.log( - 'L2 included: block=', - l2Receipt?.blockNumber, - 'status=', - l2Receipt?.status, - 'hash=', - l2Receipt?.hash, - ); - - console.log('STATUS (ready):', await sdk.withdrawals.status(created.l2TxHash)); - - // Wait until the withdrawal is ready to finalize - await sdk.withdrawals.wait(created.l2TxHash, { for: 'ready' }); - - // Finalize on L1 - const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); - if (!fin.ok) { - console.error('FINALIZE failed:', fin.error); - return; - } - console.log( - 'FINALIZE status:', - fin.value.status, - fin.value.receipt?.hash ?? '(already finalized)', - ); - - const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); - if (l1Receipt) { - console.log('L1 finalize receipt:', l1Receipt.hash); - } else { - console.log('Finalized (no local L1 receipt available, possibly finalized by another actor).'); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/ethers/withdrawals-eth.ts b/docs/snippets/ethers/withdrawals-eth.ts deleted file mode 100644 index fe4f085..0000000 --- a/docs/snippets/ethers/withdrawals-eth.ts +++ /dev/null @@ -1,74 +0,0 @@ -// examples/withdrawals-eth.ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - const l1 = new JsonRpcProvider(L1_RPC); - const l2 = new JsonRpcProvider(L2_RPC); - const signer = new Wallet(PRIVATE_KEY, l1); - - const client = createEthersClient({ l1, l2, signer }); - const sdk = createEthersSdk(client); - - const me = (await signer.getAddress()); - - // Withdraw params (ETH) - const params = { - token: ETH_ADDRESS, - amount: parseEther('0.01'), // 0.001 ETH - to: me, - // l2GasLimit: 300_000n, - } as const; - - // Quote (dry-run only) - const quote = await sdk.withdrawals.quote(params); - console.log('QUOTE: ', quote); - - const prepare = await sdk.withdrawals.prepare(params); - console.log('PREPARE: ', prepare); - - const created = await sdk.withdrawals.create(params); - console.log('CREATE:', created); - - // Quick status check - console.log('STATUS (initial):', await sdk.withdrawals.status(created.l2TxHash)); - - // wait for L2 inclusion - const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); - console.log( - 'L2 included: block=', - l2Receipt?.blockNumber, - 'status=', - l2Receipt?.status, - 'hash=', - l2Receipt?.hash, - ); - - // Optional: check status again - console.log('STATUS (post-L2):', await sdk.withdrawals.status(created.l2TxHash)); - - // finalize on L1 - // Use tryFinalize to avoid throwing in an example script - await sdk.withdrawals.wait(created.l2TxHash, { for: 'ready' }); - console.log('STATUS (ready):', await sdk.withdrawals.status(created.l2TxHash)); - - const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); - console.log('TRY FINALIZE: ', fin); - - const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); - if (l1Receipt) { - console.log('L1 finalize receipt:', l1Receipt.hash); - } else { - console.log('Finalized (no local L1 receipt available, possibly finalized by another actor).'); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/viem/deposit-erc20.ts b/docs/snippets/viem/deposit-erc20.ts deleted file mode 100644 index 8bb5edc..0000000 --- a/docs/snippets/viem/deposit-erc20.ts +++ /dev/null @@ -1,95 +0,0 @@ -// examples/deposit-erc20.ts -import { - Account, - Chain, - createPublicClient, - createWalletClient, - http, - parseUnits, - Transport, - WalletClient, -} from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { createViemSdk, createViemClient } from '@dutterbutter/zksync-sdk/viem'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - if (!PRIVATE_KEY) { - throw new Error('Set your PRIVATE_KEY in the .env file'); - } - - // --- Viem clients --- - const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); - const l1 = createPublicClient({ transport: http(L1_RPC) }); - const l2 = createPublicClient({ transport: http(L2_RPC) }); - const l1Wallet: WalletClient = createWalletClient({ - account, - transport: http(L1_RPC), - }); - - // --- SDK --- - const client = createViemClient({ l1, l2, l1Wallet }); - const sdk = createViemSdk(client); - - // sepolia example - const TOKEN = '0x42E331a2613Fd3a5bc18b47AE3F01e1537fD8873'; - - const me = account.address; - const depositAmount = parseUnits('250', 18); - - // Optional (local): mint some tokens first if your ERC-20 supports `mint(address,uint256)` - // const { request } = await l1.simulateContract({ - // address: TOKEN, - // abi: IERC20ABI as const, - // functionName: 'mint', - // args: [me, amount] as const, - // account, - // }); - // await l1Wallet.writeContract(request); - - // --- Quote --- - const quote = await sdk.deposits.quote({ token: TOKEN, to: me, amount: depositAmount }); - console.log('QUOTE:', quote); - - // --- Prepare (route + steps, no sends) --- - const prepared = await sdk.deposits.prepare({ token: TOKEN, to: me, amount: depositAmount }); - console.log('PREPARE:', prepared); - - // --- Create (prepare + send all steps) --- - const created = await sdk.deposits.create({ token: TOKEN, to: me, amount: depositAmount }); - console.log('CREATE:', created); - - // Immediate status - const status = await sdk.deposits.status(created); - console.log('STATUS (immediate):', status); - - // Wait for L1 inclusion - const l1Receipt = await sdk.deposits.wait(created, { for: 'l1' }); - console.log( - 'L1 Included at block:', - l1Receipt?.blockNumber, - 'status:', - l1Receipt?.status, - 'hash:', - l1Receipt?.transactionHash, - ); - - // Wait for L2 execution - const l2Receipt = await sdk.deposits.wait(created, { for: 'l2' }); - console.log( - 'L2 Included at block:', - l2Receipt?.blockNumber, - 'status:', - l2Receipt?.status, - 'hash:', - l2Receipt?.transactionHash, - ); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/viem/deposit-eth.ts b/docs/snippets/viem/deposit-eth.ts deleted file mode 100644 index 92da8a3..0000000 --- a/docs/snippets/viem/deposit-eth.ts +++ /dev/null @@ -1,98 +0,0 @@ -// examples/deposit-eth.ts -import { createPublicClient, createWalletClient, http, parseEther, WalletClient } from 'viem'; -import type { Account, Chain, Transport } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; - -import { createViemSdk, createViemClient } from '@dutterbutter/zksync-sdk/viem'; -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66) { - throw new Error('Set your PRIVATE_KEY in the .env file'); - } - - // --- Viem clients --- - const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); - - const l1 = createPublicClient({ transport: http(L1_RPC) }); - const l2 = createPublicClient({ transport: http(L2_RPC) }); - const l1Wallet: WalletClient = createWalletClient({ - account, - transport: http(L1_RPC), - }); - - // Check balances - const [balL1, balL2] = await Promise.all([ - l1.getBalance({ address: account.address }), - l2.getBalance({ address: account.address }), - ]); - console.log('L1 balance:', balL1.toString()); - console.log('L2 balance:', balL2.toString()); - - // client + sdk - const client = createViemClient({ l1, l2, l1Wallet }); - const sdk = createViemSdk(client); - - const me = account.address; - const params = { - amount: parseEther('0.01'), // 0.01 ETH - to: me, - token: ETH_ADDRESS, - // optional: - // l2GasLimit: 300_000n, - // gasPerPubdata: 800n, - // operatorTip: 0n, - // refundRecipient: me, - } as const; - - // Quote - const quote = await sdk.deposits.quote(params); - console.log('QUOTE response:', quote); - - // Prepare (route + steps, no sends) - const prepared = await sdk.deposits.prepare(params); - console.log('PREPARE response:', prepared); - - // Create (prepare + send) - const created = await sdk.deposits.create(params); - console.log('CREATE response:', created); - - // Status (quick check) - const status = await sdk.deposits.status(created); - console.log('STATUS response:', status); - - // Wait (L1 inclusion) - const l1Receipt = await sdk.deposits.wait(created, { for: 'l1' }); - console.log( - 'L1 Included at block:', - l1Receipt?.blockNumber, - 'status:', - l1Receipt?.status, - 'hash:', - l1Receipt?.transactionHash, - ); - - // Status again - const status2 = await sdk.deposits.status(created); - console.log('STATUS2 response:', status2); - - // Wait for L2 execution - const l2Receipt = await sdk.deposits.wait(created, { for: 'l2' }); - console.log( - 'L2 Included at block:', - l2Receipt?.blockNumber, - 'status:', - l2Receipt?.status, - 'hash:', - l2Receipt?.transactionHash, - ); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/viem/withdrawals-erc20.ts b/docs/snippets/viem/withdrawals-erc20.ts deleted file mode 100644 index d5d0fd5..0000000 --- a/docs/snippets/viem/withdrawals-erc20.ts +++ /dev/null @@ -1,103 +0,0 @@ -// examples/withdraw-erc20.ts -import { - createPublicClient, - createWalletClient, - http, - parseUnits, - type Account, - type Chain, - type Transport, - type WalletClient, -} from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { createViemSdk, createViemClient } from '@dutterbutter/zksync-sdk/viem'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -// Replace with a real **L1 ERC-20 token address** you hold on L2 -const L1_ERC20_TOKEN = '0x42E331a2613Fd3a5bc18b47AE3F01e1537fD8873'; - -async function main() { - if (!PRIVATE_KEY) { - throw new Error('Set PRIVATE_KEY (0x-prefixed) in your environment.'); - } - - // --- Viem clients --- - const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); - const l1 = createPublicClient({ transport: http(L1_RPC) }); - const l2 = createPublicClient({ transport: http(L2_RPC) }); - - const l1Wallet: WalletClient = createWalletClient({ - account, - transport: http(L1_RPC), - }); - // Need to provide an L2 wallet client for sending L2 tx - const l2Wallet = createWalletClient({ - account, - transport: http(L2_RPC), - }); - const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); - const sdk = createViemSdk(client); - - const me = account.address; - - // Resolve the L2-mapped token for an L1 ERC-20 - const l2Token = await sdk.helpers.l2TokenAddress(L1_ERC20_TOKEN); - - // Withdraw params - const params = { - token: l2Token, - amount: parseUnits('25', 18), // withdraw 25 tokens - to: me, - // l2GasLimit: 300_000n, - } as const; - - // -------- Dry runs / planning -------- - console.log('TRY QUOTE:', await sdk.withdrawals.tryQuote(params)); - console.log('QUOTE:', await sdk.withdrawals.quote(params)); - console.log('TRY PREPARE:', await sdk.withdrawals.tryPrepare(params)); - console.log('PREPARE:', await sdk.withdrawals.prepare(params)); - - // -------- Create (L2 approvals if needed + withdraw) -------- - const created = await sdk.withdrawals.create(params); - console.log('CREATE:', created); - - // Wait for L2 inclusion - const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); - console.log( - 'L2 included: block=', - l2Receipt?.blockNumber, - 'status=', - l2Receipt?.status, - 'hash=', - l2Receipt?.transactionHash, - ); - - console.log('STATUS (post-L2):', await sdk.withdrawals.status(created.l2TxHash)); - - // Wait until the withdrawal is ready to finalize - await sdk.withdrawals.wait(created.l2TxHash, { for: 'ready' }); - console.log('STATUS (ready):', await sdk.withdrawals.status(created.l2TxHash)); - - // Finalize on L1 - const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); - console.log( - 'FINALIZE:', - fin.ok ? fin.value.status : fin.error, - fin.ok ? (fin.value.receipt?.transactionHash ?? '(already finalized)') : '', - ); - - const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); - if (l1Receipt) { - console.log('L1 finalize receipt:', l1Receipt.transactionHash); - } else { - console.log('Finalized (no local L1 receipt available, possibly finalized by another actor).'); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/snippets/viem/withdrawals-eth.ts b/docs/snippets/viem/withdrawals-eth.ts deleted file mode 100644 index 7e640ec..0000000 --- a/docs/snippets/viem/withdrawals-eth.ts +++ /dev/null @@ -1,98 +0,0 @@ -// examples/viem/withdrawals-eth.ts -import { - createPublicClient, - createWalletClient, - http, - parseEther, - type Account, - type Chain, - type Transport, -} from 'viem'; -import { privateKeyToAccount, nonceManager } from 'viem/accounts'; - -import { createViemSdk, createViemClient } from '@dutterbutter/zksync-sdk/viem'; -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX -const L2_RPC = 'http://localhost:3050'; // your L2 RPC -const PRIVATE_KEY = process.env.PRIVATE_KEY || ''; - -async function main() { - if (!PRIVATE_KEY) { - throw new Error('Set your PRIVATE_KEY (0x-prefixed 32-byte) in env'); - } - - // --- Viem clients --- - const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); - - const l1 = createPublicClient({ transport: http(L1_RPC) }); - const l2 = createPublicClient({ transport: http(L2_RPC) }); - - const l1Wallet = createWalletClient({ - account, - transport: http(L1_RPC), - }); - const l2Wallet = createWalletClient({ - account, - transport: http(L2_RPC), - }); - - const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); - const sdk = createViemSdk(client); - - const me = account.address; - - // Withdraw ETH - const params = { - token: ETH_ADDRESS, - amount: parseEther('0.01'), - to: me, - // l2GasLimit: 300_000n, // optional - } as const; - - // Quote (dry run) - const quote = await sdk.withdrawals.quote(params); - console.log('QUOTE:', quote); - - // Prepare (no sends) - const plan = await sdk.withdrawals.prepare(params); - console.log('PREPARE:', plan); - - // Create (send L2 withdraw) - const created = await sdk.withdrawals.create(params); - console.log('CREATE:', created); - - // Quick status - console.log('STATUS (initial):', await sdk.withdrawals.status(created.l2TxHash)); - - // Wait for L2 inclusion - const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' }); - console.log( - 'L2 included: block=', - l2Receipt?.blockNumber, - 'status=', - l2Receipt?.status, - 'hash=', - l2Receipt?.transactionHash, - ); - - // Wait until ready to finalize - await sdk.withdrawals.wait(created.l2TxHash, { for: 'ready' }); - console.log('STATUS (ready):', await sdk.withdrawals.status(created.l2TxHash)); - - // Try to finalize on L1 - const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash); - console.log('TRY FINALIZE:', fin); - - const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' }); - if (l1Receipt) { - console.log('L1 finalize receipt:', l1Receipt.transactionHash); - } else { - console.log('Finalized (no local L1 receipt — possibly finalized by someone else).'); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md deleted file mode 100644 index 6ed786b..0000000 --- a/docs/src/SUMMARY.md +++ /dev/null @@ -1,40 +0,0 @@ -# Summary - -- [Overview](overview/index.md) - - [What this SDK does](overview/what-it-does.md) - - [Mental model](overview/mental-model.md) - - [Adapters (viem & ethers)](overview/adapters.md) - -- [Quickstart](quickstart/index.md) - - [Choose your adapter](quickstart/choose-adapter.md) - - [Quickstart (viem)](quickstart/viem.md) - - [Quickstart (ethers)](quickstart/ethers.md) - -- [How-to Guides](guides/index.md) - - [Deposits]() - - [viem](guides/deposits/viem.md) - - [ethers](guides/deposits/ethers.md) - - [Withdrawals]() - - [viem](guides/withdrawals/viem.md) - - [ethers](guides/withdrawals/ethers.md) - -- [Concepts](concepts/index.md) - - [Status vs Wait](concepts/status-vs-wait.md) - - [Finalization](concepts/finalization.md) - -- [Reference](reference/index.md) - - [ZKsync RPC Helpers](reference/methods.md) - - [Common Helpers](reference/helpers.md) - - diff --git a/docs/src/concepts/finalization.md b/docs/src/concepts/finalization.md deleted file mode 100644 index d0792fb..0000000 --- a/docs/src/concepts/finalization.md +++ /dev/null @@ -1,44 +0,0 @@ -# Finalization (Withdrawals) - -When withdrawing from ZKsync (L2) back to Ethereum (L1), **your funds are not automatically released on L1** once the L2 transaction is included. - -Withdrawals are always a **two-step process**: - -1. **Initiate on L2** — you call `withdraw()` (via the SDK’s `create`) to start the withdrawal. - - This burns/locks the funds on L2. - - At this point, your withdrawal is visible in L2 receipts and logs, but **your funds are not yet available on L1**. - -2. **Finalize on L1** — you must explicitly call `finalize` to release your funds on L1. - - This submits an L1 transaction. - - Only after this step does your ETH or token balance increase on Ethereum. - -## Why finalization matters - -- **Funds remain locked** until finalization. -- **Anyone can finalize** — not just the withdrawer. In practice, most users will finalize their own withdrawals. -- **Finalization costs gas on L1**, so plan for this when withdrawing. - -If you **forget to finalize**, your funds will stay in limbo: visible as “ready to withdraw,” but unavailable on Ethereum. - -## SDK methods - -- **`finalize(l2TxHash)`** - Actively sends the L1 transaction to finalize the withdrawal. Returns the updated `status` and the L1 receipt. - -## Example: Explicit finalize - -```ts -// Step 1: Create withdrawal on L2 -const withdrawal = await sdk.withdrawals.create({ - token: ETH_ADDRESS, - amount: parseEther('0.1'), - to: myAddress, -}); - -// Step 2: Finalize on L1 -await sdk.withdrawals.wait(withdrawal, { for: 'ready' }); // block until finalizable -const { status, receipt } = await sdk.withdrawals.finalize(withdrawal.l2TxHash); - -console.log(status.phase); // "FINALIZED" -console.log(receipt?.transactionHash); // L1 finalize tx -``` diff --git a/docs/src/concepts/index.md b/docs/src/concepts/index.md deleted file mode 100644 index 12b7325..0000000 --- a/docs/src/concepts/index.md +++ /dev/null @@ -1,13 +0,0 @@ -# Concepts - -This section explains the small set of ideas you need to use the SDK confidently. Keep these in mind as you read the guides and API reference. - -## What’s here - -- **[Status vs Wait](status-vs-wait.md)** - When to take a quick, non-blocking snapshot (`status`) vs when to block until a checkpoint (`wait`). - Covers deposit phases (`L1_PENDING → L2_EXECUTED/FAILED`) and withdrawal phases (`L2_PENDING → READY_TO_FINALIZE → FINALIZED`), polling options, and return shapes. - -- **[Finalization](finalization.md)** - Withdrawals are **two-step**: initiate on L2, then **you must call `finalize` on L1** to release funds. - Explains readiness, how to detect `READY_TO_FINALIZE`, and how to use `finalize`. diff --git a/docs/src/concepts/status-vs-wait.md b/docs/src/concepts/status-vs-wait.md deleted file mode 100644 index 80acc58..0000000 --- a/docs/src/concepts/status-vs-wait.md +++ /dev/null @@ -1,117 +0,0 @@ -# Status vs Wait - -The SDK exposes two complementary ways to track progress: - -- **`status(...)`** — returns a **non-blocking snapshot** of where an operation is. -- **`wait(..., { for })`** — **blocks/polls** until a specified checkpoint is reached. - -Both methods work for **Deposits** and **Withdrawals**, but Withdrawals add finalization-specific states and targets. - -## Withdrawals - -### `withdrawals.status(h | l2TxHash) → Promise` - -**Input** - -- `h`: a `WithdrawalWaitable` (e.g., from `create`) **or** the L2 tx hash `Hex`. - -**Phases returned** - -- `UNKNOWN` — no L2 hash available on the handle. -- `L2_PENDING` — L2 tx not yet included. -- `PENDING` — L2 included, but not yet ready to finalize. -- `READY_TO_FINALIZE` — finalization would succeed now. -- `FINALIZED` — finalized on L1 (funds released). - -**Notes** - -- When L2 receipt is missing → `L2_PENDING`. -- When finalization key can be derived but not ready → `PENDING`. -- When already finalized → `FINALIZED`. - -**Example** - -```ts -const s = await sdk.withdrawals.status(handleOrHash); -// s.phase in: 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' -``` - -### `withdrawals.wait(h | l2TxHash, { for, pollMs?, timeoutMs? })` - -**Targets** - -- `{ for: 'l2' }` → resolves with **L2 receipt** (`TransactionReceiptZKsyncOS | null`) -- `{ for: 'ready' }` → resolves **`null`** when finalization becomes possible -- `{ for: 'finalized' }` → resolves **L1 receipt** when finalized, or `null` if finalized but receipt not found - -**Behavior** - -- If the handle has **no L2 tx hash**, returns `null` immediately. -- Default polling interval: 5500ms default or set explicitly if you want. -- Optional `timeoutMs` returns `null` on deadline. - -**Example** - -```ts -// wait for inclusion on L2, get L2 receipt (augmented with l2ToL1Logs if available) -const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2', pollMs: 5000 }); - -// wait until it's available to finalize (no side-effects) -await sdk.withdrawals.wait(handle, { for: 'ready' }); - -// wait until finalized; returns L1 receipt (or null if finalized but receipt not retrievable) -const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized' }); -``` - -**Common Troubleshooting** - -- Network hiccup while fetching receipts → thrown `ZKsyncError` (`RPC` kind). -- Internal decode issue → thrown `ZKsyncError` (`INTERNAL` kind). - ---- - -## Deposits - -### `deposits.status(h | l1TxHash) → Promise` - -**Input** - -- `h`: `DepositWaitable` (from `create`) **or** L1 tx hash `Hex`. - -**Phases returned** - -- `UNKNOWN` — no L1 hash. -- `L1_PENDING` — L1 receipt missing. -- `L1_INCLUDED` — L1 included, but L2 hash not yet derivable from logs. -- `L2_PENDING` — L2 hash known but receipt missing. -- `L2_EXECUTED` — L2 receipt present with `status === 1`. -- `L2_FAILED` — L2 receipt present with `status !== 1`. - -**Example** - -```ts -const s = await sdk.deposits.status(handleOrL1Hash); -// s.phase in: 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' -``` - -### `deposits.wait(h | l1TxHash, { for: 'l1' | 'l2' })` - -**Targets** - -- `{ for: 'l1' }` → waits for L1 inclusion → **L1 receipt** or `null` -- `{ for: 'l2' }` → waits L1 inclusion **and** canonical L2 execution → **L2 receipt** or `null` - -**Example** - -```ts -const l1Rcpt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Rcpt = await sdk.deposits.wait(handle, { for: 'l2' }); -``` - ---- - -## Tips & edge cases - -- **Handles vs hashes:** Both methods accept either a handle (from `create`) or a raw tx hash (`Hex`). If you pass a handle without the relevant hash, you’ll get `UNKNOWN`/`null`. -- **Polling:** For withdrawals, set `pollMs` explicitly if you want tighter/looser polling; minimum enforced is **5500ms**. -- **Timeouts:** Use `timeoutMs` for long waits (e.g., finalization windows) to avoid hanging scripts. diff --git a/docs/src/guides/deposits/ethers.md b/docs/src/guides/deposits/ethers.md deleted file mode 100644 index 8b220ec..0000000 --- a/docs/src/guides/deposits/ethers.md +++ /dev/null @@ -1,124 +0,0 @@ -# Deposits (ethers) - -A fast path to deposit **ETH / ERC-20** from L1 → ZKsync (L2) using the **ethers** adapter. - -## Prerequisites - -- A funded **L1** account (gas + amount). -- RPC URLs: `L1_RPC_URL`, `L2_RPC_URL`. -- Installed: `@dutterbutter/zksync-sdk` + `ethers`. - ---- - -## Parameters (quick reference) - -| Param | Required | Meaning | -| ----------------- | -------- | -------------------------------------- | -| `token` | Yes | `ETH_ADDRESS` or ERC-20 address | -| `amount` | Yes | BigInt/wei (e.g. `parseEther('0.01')`) | -| `to` | Yes | L2 recipient address | -| `l2GasLimit` | No | L2 execution gas cap | -| `gasPerPubdata` | No | Pubdata price hint | -| `operatorTip` | No | Optional tip to operator | -| `refundRecipient` | No | L2 address to receive fee refunds | - -> ERC-20 deposits may require an L1 `approve()`. **`quote()`** surfaces required steps. - ---- - -## Fast path (one-shot) - -```ts -{{#include ../../../snippets/ethers/deposit-eth.ts}} -``` - -- `create()` prepares **and** sends. -- `wait(..., { for: 'l1' })` ⇒ included on L1. -- `wait(..., { for: 'l2' })` ⇒ executed on L2 (funds available). - -## Inspect & customize (quote → prepare → create) - -**1. Quote (no side-effects)** -Preview fees/steps and whether an approve is required. - -```ts -const quote = await sdk.deposits.quote(params); -``` - -**2. Prepare (build txs, don’t send)** -Get `TransactionRequest[]` for signing/UX. - -```ts -const plan = await sdk.deposits.prepare(params); -``` - -**3. Create (send)** -Use defaults, or send your prepared txs if you customized. - -```ts -const handle = await sdk.deposits.create(params); -``` - -## Track progress (status vs wait) - -**Non-blocking snapshot** - -```ts -const s = await sdk.deposits.status(handle /* or l1TxHash */); -// 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' -``` - -**Block until checkpoint** - -```ts -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); -``` - ---- - -## Error handling patterns - -**Exceptions** - -```ts -try { - const handle = await sdk.deposits.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, revert?) -} -``` - -**No-throw style** - -Every method has a `try*` variant (e.g. `tryQuote`, `tryPrepare`, `tryCreate`). -These never throw—so you don’t need a `try/catch`. Instead they return: - -- `{ ok: true, value: ... }` on success -- `{ ok: false, error: ... }` on failure - -This is useful for **UI flows** or **services** where you want explicit control over errors. - -```ts -const r = await sdk.deposits.tryCreate(params); - -if (!r.ok) { - // handle the error gracefully - console.error('Deposit failed:', r.error); - // maybe show a toast, retry, etc. -} else { - const handle = r.value; - console.log('Deposit sent. L1 tx hash:', handle.l1TxHash); -} -``` - -## Troubleshooting - -- **Stuck at L1:** check L1 gas and RPC health. -- **No L2 execution:** verify L2 RPC; re-check `status()` (should move to `L2_EXECUTED`). -- **L2 failed:** `status.phase === 'L2_FAILED'` → inspect revert info via your error envelope/logs. - -## See also - -- [Status vs Wait](../../concepts/status-vs-wait.md) -- [ZKsync RPC Helpers](../../zks/methods.md) diff --git a/docs/src/guides/deposits/index.md b/docs/src/guides/deposits/index.md deleted file mode 100644 index ccd3f0a..0000000 --- a/docs/src/guides/deposits/index.md +++ /dev/null @@ -1 +0,0 @@ -# Deposits diff --git a/docs/src/guides/deposits/viem.md b/docs/src/guides/deposits/viem.md deleted file mode 100644 index dbb19b8..0000000 --- a/docs/src/guides/deposits/viem.md +++ /dev/null @@ -1,121 +0,0 @@ -# Deposits (viem) - -A fast path to deposit **ETH / ERC-20** from L1 → ZKsync (L2) using the **viem** adapter. - -## Prerequisites - -- A funded **L1** account (gas + amount). -- RPC URLs: `L1_RPC_URL`, `L2_RPC_URL`. -- Installed: `@dutterbutter/zksync-sdk` + `viem`. - ---- - -## Parameters (quick reference) - -| Param | Required | Meaning | -| ----------------- | -------- | -------------------------------------- | -| `token` | Yes | `ETH_ADDRESS` or ERC-20 address | -| `amount` | Yes | BigInt/wei (e.g. `parseEther('0.01')`) | -| `to` | Yes | L2 recipient address | -| `l2GasLimit` | No | L2 execution gas cap | -| `gasPerPubdata` | No | Pubdata price hint | -| `operatorTip` | No | Optional tip to operator | -| `refundRecipient` | No | L2 address to receive fee refunds | - -> ERC-20 deposits may require an L1 `approve()`. **`quote()`** surfaces required steps. - -## Fast path (one-shot) - -```ts -{{#include ../../../snippets/viem/deposit-eth.ts}} -``` - -- `create()` prepares **and** sends. -- `wait(..., { for: 'l1' })` ⇒ included on L1. -- `wait(..., { for: 'l2' })` ⇒ executed on L2 (funds available). - -## Inspect & customize (quote → prepare → create) - -**1. Quote (no side-effects)** - -Preview fees/steps and whether an approve is required. - -```ts -const quote = await sdk.deposits.quote(params); -``` - -**2. Prepare (build txs, don’t send)** -Get `TransactionRequest[]` for signing/UX. - -```ts -const plan = await sdk.deposits.prepare(params); -``` - -**3. Create (send)** -Use defaults, or send your prepared txs if you customized. - -```ts -const handle = await sdk.deposits.create(params); -``` - -## Track progress (status vs wait) - -**Non-blocking snapshot** - -```ts -const s = await sdk.deposits.status(handle /* or l1TxHash */); -// 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' -``` - -**Block until checkpoint** - -```ts -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); -``` - -## Error handling patterns - -**Exceptions** - -```ts -try { - const handle = await sdk.deposits.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, revert?) -} -``` - -**No-throw style** - -Every method has a `try*` variant (e.g. `tryQuote`, `tryPrepare`, `tryCreate`). -These never throw—so you don’t need a `try/catch`. Instead they return: - -- `{ ok: true, value: ... }` on success -- `{ ok: false, error: ... }` on failure - -This is useful for **UI flows** or **services** where you want explicit control over errors. - -```ts -const r = await sdk.deposits.tryCreate(params); - -if (!r.ok) { - // handle the error gracefully - console.error('Deposit failed:', r.error); - // maybe show a toast, retry, etc. -} else { - const handle = r.value; - console.log('Deposit sent. L1 tx hash:', handle.l1TxHash); -} -``` - -## Troubleshooting - -- **Stuck at L1:** check L1 gas and RPC health. -- **No L2 execution:** verify L2 RPC; re-check `status()` (should move to `L2_EXECUTED`). -- **L2 failed:** `status.phase === 'L2_FAILED'` → inspect revert info via your error envelope/logs. - -## See also - -- [Status vs Wait](../../concepts/status-vs-wait.md) -- [ZKsync RPC Helpers](../../zks/methods.md) diff --git a/docs/src/guides/index.md b/docs/src/guides/index.md deleted file mode 100644 index 7d51c99..0000000 --- a/docs/src/guides/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# How-to Guides - -This section provides **task-focused recipes** for deposits and withdrawals with the adapter of your choice. - -Each guide shows the minimal steps to accomplish a task using the SDK, with real code you can copy, paste, and run. - -## When to use Guides - -- Use these guides if you want to **get something working quickly** (e.g., deposit ETH, withdraw ERC-20). -- If you need a deeper explanation of the SDK’s design, check [Concepts](../concepts/index.md). - -## Available Guides - -### Deposits - -- [Deposits with viem](./deposits/viem.md) -- [Deposits with ethers](./deposits/ethers.md) - -### Withdrawals - -- [Withdrawals with viem](./withdrawals/viem.md) -- [Withdrawals with ethers](./withdrawals/ethers.md) diff --git a/docs/src/guides/withdrawals/ethers.md b/docs/src/guides/withdrawals/ethers.md deleted file mode 100644 index a5832ad..0000000 --- a/docs/src/guides/withdrawals/ethers.md +++ /dev/null @@ -1,127 +0,0 @@ -# Withdrawals (ethers) - -A fast path to withdraw **ETH / ERC-20** from ZKsync (L2) → Ethereum (L1) using the **ethers** adapter. - -Withdrawals are a **two-step process**: - -1. **Initiate** on L2. -2. **Finalize** on L1 to release funds. - -## Prerequisites - -- A funded **L2** account to initiate the withdrawal. -- A funded **L1** account for finalization. -- RPC URLs: `L1_RPC_URL`, `L2_RPC_URL`. -- Installed: `@dutterbutter/zksync-sdk` + `ethers`. - ---- - -## Parameters (quick reference) - -| Param | Required | Meaning | -| ----------------- | -------- | ------------------------------------------------- | -| `token` | Yes | `ETH_ADDRESS` or ERC-20 address | -| `amount` | Yes | BigInt/wei (e.g. `parseEther('0.01')`) | -| `to` | Yes | L1 recipient address | -| `refundRecipient` | No | L2 address to receive fee refunds (if applicable) | - -## Fast path (one-shot) - -```ts -{{#include ../../../snippets/ethers/withdrawals-eth.ts}} -``` - -- `create()` prepares **and** sends the L2 withdrawal. -- `wait(..., { for: 'l2' })` ⇒ included on L2. -- `wait(..., { for: 'ready' })` ⇒ ready for finalization. -- `finalize(l2TxHash)` ⇒ required to release funds on L1. - -## Inspect & customize (quote → prepare → create) - -**1. Quote (no side-effects)** - -```ts -const quote = await sdk.withdrawals.quote(params); -``` - -**2. Prepare (build txs, don’t send)** - -```ts -const plan = await sdk.withdrawals.prepare(params); -``` - -**3. Create (send)** - -```ts -const handle = await sdk.withdrawals.create(params); -``` - -## Track progress (status vs wait) - -**Non-blocking snapshot** - -```ts -const s = await sdk.withdrawals.status(handle /* or l2TxHash */); -// 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' -``` - -**Block until checkpoint** - -```ts -const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); -await sdk.withdrawals.wait(handle, { for: 'ready' }); -``` - -## Finalization (required step) - -```ts -const result = await sdk.withdrawals.finalize(handle.l2TxHash); -console.log('Finalization result:', result); -``` - -## Error handling patterns - -**Exceptions** - -```ts -try { - const handle = await sdk.withdrawals.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, optional revert) -} -``` - -**No-throw style** - -Use `try*` methods to avoid exceptions. They return `{ ok, value }` or `{ ok, error }`. -Perfect for UIs or services that prefer explicit flow control. - -```ts -const r = await sdk.withdrawals.tryCreate(params); - -if (!r.ok) { - console.error('Withdrawal failed:', r.error); -} else { - const handle = r.value; - const f = await sdk.withdrawals.tryFinalize(handle.l2TxHash); - if (!f.ok) { - console.error('Finalize failed:', f.error); - } else { - console.log('Withdrawal finalized on L1:', f.value.receipt?.transactionHash); - } -} -``` - -## Troubleshooting - -- **Never reaches READY_TO_FINALIZE:** proofs may not be available yet. -- **Finalize reverts:** ensure enough L1 gas; inspect revert info. -- **Finalized but no receipt:** `wait(..., { for: 'finalized' })` may return `null`; retry or rely on `finalize()` result. - ---- - -## See also - -- [Status vs Wait](../../concepts/status-vs-wait.md) -- [Finalization](../../concepts/finalization.md) -- [ZKsync RPC Helpers](../../zks/methods.md) diff --git a/docs/src/guides/withdrawals/index.md b/docs/src/guides/withdrawals/index.md deleted file mode 100644 index f0bfe8a..0000000 --- a/docs/src/guides/withdrawals/index.md +++ /dev/null @@ -1 +0,0 @@ -# Withdrawals diff --git a/docs/src/guides/withdrawals/viem.md b/docs/src/guides/withdrawals/viem.md deleted file mode 100644 index 3324a3a..0000000 --- a/docs/src/guides/withdrawals/viem.md +++ /dev/null @@ -1,138 +0,0 @@ -# Withdrawals (viem) - -A fast path to withdraw **ETH / ERC-20** from ZKsync (L2) → Ethereum (L1) using the **viem** adapter. - -Withdrawals are a **two-step process**: - -1. **Initiate** on L2. -2. **Finalize** on L1 to release funds. - -## Prerequisites - -- A funded **L2** account to initiate the withdrawal. -- A funded **L1** account for finalization. -- RPC URLs: `L1_RPC_URL`, `L2_RPC_URL`. -- Installed: `@dutterbutter/zksync-sdk` + `viem`. - ---- - -## Parameters (quick reference) - -| Param | Required | Meaning | -| ----------------- | -------- | ------------------------------------------------- | -| `token` | Yes | `ETH_ADDRESS` or ERC-20 address | -| `amount` | Yes | BigInt/wei (e.g. `parseEther('0.01')`) | -| `to` | Yes | L1 recipient address | -| `refundRecipient` | No | L2 address to receive fee refunds (if applicable) | - -## Fast path (one-shot) - -```ts -{{#include ../../../snippets/viem/withdrawals-eth.ts}} -``` - -- `create()` prepares **and** sends the L2 withdrawal. -- `wait(..., { for: 'l2' })` ⇒ included on L2. -- `wait(..., { for: 'ready' })` ⇒ ready for finalization. -- `finalize(l2TxHash)` ⇒ required to release funds on L1. - -## Inspect & customize (quote → prepare → create) - -**1. Quote (no side-effects)** - -Preview fees/steps and whether extra approvals are required. - -```ts -const quote = await sdk.withdrawals.quote(params); -``` - -**2. Prepare (build txs, don’t send)** - -Get `TransactionRequest[]` for signing/UX. - -```ts -const plan = await sdk.withdrawals.prepare(params); -``` - -**3. Create (send)** - -Use defaults, or send your prepared txs if you customized. - -```ts -const handle = await sdk.withdrawals.create(params); -``` - -## Track progress (status vs wait) - -**Non-blocking snapshot** - -```ts -const s = await sdk.withdrawals.status(handle /* or l2TxHash */); -// 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' -``` - -**Block until checkpoint** - -```ts -const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); -await sdk.withdrawals.wait(handle, { for: 'ready' }); // becomes finalizable -``` - -## Finalization (required step) - -To actually release funds on L1, call `finalize`. Note -the transaction needs to be ready for finalization. - -```ts -const result = await sdk.withdrawals.finalize(handle.l2TxHash); -console.log('Finalization status:', result.status.phase); -``` - -## Error handling patterns - -**Exceptions** - -```ts -try { - const handle = await sdk.withdrawals.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, optional revert) -} -``` - -**No-throw style** - -Every method has a `try*` variant (e.g. `tryQuote`, `tryPrepare`, `tryCreate`, `tryFinalize`). -These never throw—so you don’t need `try/catch`. Instead they return: - -- `{ ok: true, value: ... }` on success -- `{ ok: false, error: ... }` on failure - -This is useful for **UI flows** or **services** where you want explicit control over errors. - -```ts -const r = await sdk.withdrawals.tryCreate(params); - -if (!r.ok) { - console.error('Withdrawal failed:', r.error); -} else { - const handle = r.value; - const f = await sdk.withdrawals.tryFinalize(handle.l2TxHash); - if (!f.ok) { - console.error('Finalize failed:', f.error); - } else { - console.log('Withdrawal finalized on L1:', f.value.receipt?.transactionHash); - } -} -``` - -## Troubleshooting - -- **Never reaches READY_TO_FINALIZE:** proofs may not be available yet; poll `status()` or `wait(..., { for: 'ready' })`. -- **Finalize fails:** ensure you have L1 gas and check revert info in the error envelope. - -## See also - -- [Status vs Wait](../../concepts/status-vs-wait.md) -- [Finalization](../../concepts/finalization.md) -- [ZKsync RPC Helpers](../../zks/methods.md) diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index 6a4b980..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# ZKsync SDK - -Welcome! Use the sidebar for API reference. - -- **Core**: encoding, attributes, bundle builders. -- **Ethers**: high-level send helpers. diff --git a/docs/src/overview/adapters.md b/docs/src/overview/adapters.md deleted file mode 100644 index 30c24fd..0000000 --- a/docs/src/overview/adapters.md +++ /dev/null @@ -1,88 +0,0 @@ -# Adapters: `viem` & `ethers` - -The SDK is designed to work _with_ the tools you already know and love. It's not a standalone library, but rather an extension that plugs into your existing `viem` or `ethers.js` setup. - -Think of it like a power adapter 🔌. You have your device (`viem` or `ethers` client), and this SDK adapts it to work seamlessly with zkSync's unique features. You bring your own client, and the SDK enhances it. - -## Why an Adapter Model? - -This approach offers several key advantages: - -- ✅ **Bring Your Own Stack:** You don't have to replace your existing setup. The SDK integrates directly with the `viem` clients (`PublicClient`, `WalletClient`) or `ethers` providers and signers you're already using. -- 📚 **Familiar Developer Experience (DX):** You continue to handle connections, accounts, and signing just as you always have. -- 🧩 **Lightweight & Focused:** The SDK remains small and focused on one thing: providing a robust API for ZKsync-specific actions like deposits and withdrawals. - -## Installation - -First, install the core SDK, then add the adapter that matches your project's stack. - -```bash -# For viem users -npm install @dutterbutter/zksync-sdk viem - -# For ethers.js users -npm install @dutterbutter/zksync-sdk ethers -``` - -## How to Use - -The SDK extends your existing client. Configure **viem** or **ethers** as you normally would, then pass them into the adapter’s client factory and create the SDK surface. - -### viem (public + wallet client) - -```ts -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 account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); - -const l1 = createPublicClient({ transport: http(process.env.L1_RPC!) }); -const l2 = createPublicClient({ transport: http(process.env.L2_RPC!) }); -const l1Wallet = createWalletClient({ account, transport: http(process.env.L1_RPC!) }); - -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); - -const params = { - amount: parseEther('0.01'), - to: account.address, - token: ETH_ADDRESS, -} as const; - -const handle = await sdk.deposits.create(params); -await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 -``` - -### ethers (providers + signer) - -```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const l1 = new JsonRpcProvider(process.env.L1_RPC!); -const l2 = new JsonRpcProvider(process.env.L2_RPC!); -const signer = new Wallet(process.env.PRIVATE_KEY!, l1); - -const client = await createEthersClient({ l1, l2, signer }); -const sdk = createEthersSdk(client); - -const params = { - amount: parseEther('0.01'), - to: await signer.getAddress(), - token: ETH_ADDRESS, -} as const; - -const handle = await sdk.deposits.create(params); -await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 -``` - ---- - -## Key Principles - -- **No Key Management:** The SDK never asks for or stores private keys. All signing operations are delegated to the `viem` `WalletClient` or `ethers` `Signer` you provide. -- **API Parity:** Both adapters expose the exact same API. The code you write to call `client.deposits.quote()` is identical whether you're using `viem` or `ethers`. -- **Easy Migration:** Because the API is the same, switching your project from `ethers` to `viem` (or vice versa) is incredibly simple. You only need to change the initialization code. diff --git a/docs/src/overview/index.md b/docs/src/overview/index.md deleted file mode 100644 index 2af46b8..0000000 --- a/docs/src/overview/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# Overview - -The `@zksync-sdk` is a lightweight extension for `viem` and `ethers` that makes ZKsync cross-chain actions simple and consistent. - -Instead of re-implementing accounts or RPC logic, this SDK focuses only on **ZKsync-specific flows**: - -- Deposits (L1 → L2) -- Withdrawals (L2 → L1, with finalization) -- Status & wait helpers -- ZKsync specific JSON-RPC methods - -## What you’ll find here - -- [**What this SDK does**](./what-it-does.md) — the purpose, scope, and non-goals. -- [**Mental model**](./mental-model.md) — how to think about the core methods (`quote → prepare → create → status → wait → finalize`). -- [**Adapters (viem & ethers)**](./adapters.md) — how the SDK integrates with your existing stack. - -## Next steps - -👉 If you want to get hands-on right away, jump to the [Quickstart](../quickstart/index.md). diff --git a/docs/src/overview/mental-model.md b/docs/src/overview/mental-model.md deleted file mode 100644 index 1b9426c..0000000 --- a/docs/src/overview/mental-model.md +++ /dev/null @@ -1,121 +0,0 @@ -# Mental Model - -The SDK is designed around a predictable and layered API for handling L1-L2, and L2-L1 operations. Every action, whether it's a deposit or a withdrawal, follows a consistent lifecycle. Understanding this lifecycle is key to using the SDK effectively. - -The complete lifecycle for any action is: - -```bash -quote → prepare → create → status → wait → (finalize*) -``` - -- The first five steps are common to both **Deposits** and **Withdrawals**. -- Withdrawals require an additional **`finalize`** step to prove and claim the funds on L1. - -You can enter this lifecycle at different stages depending on how much control you need. - -## The Core API: A Layered Approach - -The core methods are designed to give you progressively more automation. You can start by just getting information (`quote`), move to building transactions without sending them (`prepare`), or execute the entire flow with a single call (`create`). - -### `quote(params)` - -_"What will this operation involve and cost?"_ - -This is a **read-only** dry run. It performs no transactions and has no side effects. It inspects the parameters and returns a `Quote` object containing the estimated fees, gas costs, and the steps the SDK will take to complete the action. - -➡️ **Best for:** Displaying a confirmation screen to a user with a cost estimate before they commit. - -### `prepare(params)` - -_"Build the transactions for me, but let me send them."_ - -This method constructs all the necessary transactions for the operation and returns them as an array of `TransactionRequest` objects in a `Plan`. It does **not** sign or send them. This gives you full control over the final execution. - -➡️ **Best for:** Custom workflows where you need to inspect transactions before signing, use a unique signing method, or submit them through a separate system (like a multisig). - -### `create(params)` - -_"Prepare, sign, and send in one go."_ - -This is the most common entry point for a one-shot operation. It internally calls `prepare`, then uses your configured signer to sign and dispatch the transactions. It returns a `Handle` object, which is a lightweight tracker containing the transaction hash(es) needed for the next steps. - -➡️ **Best for:** Most standard use cases where you simply want to initiate the deposit or withdrawal. - -### `status(handle | txHash)` - -_"Where is my transaction right now?"_ - -This is a **non-blocking** check to get the current state of an operation. It takes a `Handle` from the `create` method or a transaction hash and returns a structured status object, such as: - -- **Deposits:** `{ phase: 'L1_PENDING' | 'L2_EXECUTED' }` -- **Withdrawals:** `{ phase: 'L1_INCLUDED','L2_PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' }` - -➡️ **Best for:** Polling in a UI to show a user the live progress of their transaction without blocking the interface. - -### `wait(handle, { for })` - -_"Pause until a specific checkpoint is reached."_ - -This is a **blocking** (asynchronous) method that polls for you. It pauses execution until the operation reaches a desired checkpoint and then resolves with the relevant transaction receipt. - -- **Deposits:** Wait for L1 inclusion (`'l1'`) or L2 execution (`'l2'`). -- **Withdrawals:** Wait for L2 inclusion (`'l2'`), finalization availability (`'ready'`), or final L1 finalization (`'finalized'`). - -➡️ **Best for:** Scripts or backend processes where you need to ensure one step is complete before starting the next. - -### `finalize(l2TxHash)` - -_(Withdrawals Only)_ - -_"My funds are ready on L1. Finalize and release them."_ - -This method executes the final step of a withdrawal. After `status` reports `READY_TO_FINALIZE`, you call this method with the L2 transaction hash to submit the finalization transaction on L1, which releases the funds to the recipient. - -➡️ **Best for:** The final step of any withdrawal flow. - -## Error Handling: The `try*` Philosophy - -For more robust error handling without `try/catch` blocks, **every core method has a `try*` variant** (e.g., `tryQuote`, `tryCreate`). - -Instead of throwing an error on failure, these methods return a result object that enforces explicit error handling: - -```ts -// Instead of this: -try { - const handle = await sdk.withdrawals.create(params); - // ... happy path -} catch (error) { - // ... sad path -} - -// You can do this: -const result = await sdk.withdrawals.tryCreate(params); - -if (result.ok) { - // Safe to use result.value, which is the WithdrawHandle - const handle = result.value; -} else { - // Handle the error explicitly - console.error('Withdrawal failed:', result.error); -} -``` - -➡️ **Best for:** Applications that prefer a functional error-handling pattern and want to avoid uncaught exceptions. - -## Putting It All Together - -These primitives allow you to compose flows that are as simple or as complex as you need. - -#### Simple Flow - -Use `create` and `wait` for the most straightforward path. - -```ts -// 1. Create the deposit -const depositHandle = await sdk.deposits.create(params); - -// 2. Wait for it to be finalized on L2 -const receipt = await sdk.deposits.wait(depositHandle, { for: 'l2' }); - -console.log('Deposit complete!'); -``` diff --git a/docs/src/overview/what-it-does.md b/docs/src/overview/what-it-does.md deleted file mode 100644 index d937396..0000000 --- a/docs/src/overview/what-it-does.md +++ /dev/null @@ -1,47 +0,0 @@ -# What this SDK Does - -The `@zksync-sdk` is a lightweight, powerful extension for the popular `ethers.js` and `viem` libraries. Its purpose is to simplify the development of applications on ZKsync by providing straightforward access to ZKsync-specific features that are not natively available in the core Ethereum SDKs. - -Think of it as a specialized toolkit that sits on top of the tools you already know and love, enabling you to seamlessly interact with both L1 and L2 functionalities of the Elastic Network. - -## Audience - -This SDK is designed for **Web3 developers, dApp builders, and infrastructure engineers** who are building applications on or interacting with the Elastic Network. If you're comfortable with `ethers.js` or `viem` and need to implement ZKsync-specific actions, this library is for you. - -## Scope - -The SDK currently supports ZKsync specific actions, primarily L1-L2, and L2-L1 transactions. - -### Key Supported Features - -- **Deposits (L1 → L2)** — ETH and ERC-20 - - **Initiate on L1:** build and send the deposit transaction from Ethereum. - - **Track progress:** query intermediate states (queued, included, executed). - - **Verify completion on L2:** confirm funds credited/available on ZKsync (L2). - -- **Withdrawals (L2 → L1)** — ETH and ERC-20 - - **Initiate on L2:** create the withdrawal transaction on ZKsync (L2). - - **Track progress:** monitor execution and finalization availability. - - **Finalize on L1:** Finalize withdrawal to release funds (L1). - -- **ZKsync RPC** - - **`getBridgehubAddress`** (`zks_getBridgehubContract`) - Resolve the canonical Bridgehub contract address. - - **`getL2ToL1LogProof`** (`zks_getL2ToL1LogProof`) - Retrieves the log proof for an L2 to L1 transaction. - - **`getReceiptWithL2ToL1`** _(receipt extension)_ - Returns an Ethereum `TransactionReceipt` **augmented** with `l2ToL1Logs`. - -## Non-Goals - -To maintain its focus and lightweight nature, this SDK explicitly avoids duplicating functionality that is already well-handled by `ethers.js`, `viem`, or other dedicated libraries. - -The following are **out of scope**: - -- **Wallet Management & Signing:** The SDK does not manage private keys, mnemonics, or other sensitive credentials. It expects a pre-configured Signer or Wallet Client from `ethers` or `viem`. Key storage and transaction signing are delegated to these underlying libraries. -- **Generic Ethereum Interactions:** Standard Ethereum transactions, contract calls, or RPC methods that are not specific to ZKsync should be handled directly by `ethers` or `viem`. - ---- - -ℹ️ Runtime compatibility follows the adapter you choose (`viem` or `ethers`). -See their docs for environment support. diff --git a/docs/src/quickstart/choose-adapter.md b/docs/src/quickstart/choose-adapter.md deleted file mode 100644 index bc3f191..0000000 --- a/docs/src/quickstart/choose-adapter.md +++ /dev/null @@ -1,68 +0,0 @@ -# Choosing Your Adapter: `viem` vs. `ethers` - -The SDK is designed to work with both `viem` and `ethers.js`, the two most popular Ethereum libraries. Since the SDK offers **identical functionality** for both, the choice comes down to your project's needs and your personal preference. - -## The Short Answer (TL;DR) - -- **If you're adding the SDK to an existing project:** Use the adapter for the library you're already using. -- **If you're starting a new project:** The choice is yours. `viem` is generally recommended for new projects due to its modern design, smaller bundle size, and excellent TypeScript support. - -You can't make a wrong choice. Both adapters are fully supported and provide the same features. - -## Code Comparison - -The only difference in your code is the initial setup. **All subsequent SDK calls are identical.** - -#### viem - -```ts -import { createPublicClient, createWalletClient, http } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { createViemClient, createViemSdk } from '@dutterbutter/zksync-sdk/viem'; - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); - -const l1 = createPublicClient({ transport: http(process.env.L1_RPC!) }); -const l2 = createPublicClient({ transport: http(process.env.L2_RPC!) }); -const l1Wallet = createWalletClient({ account, transport: http(process.env.L1_RPC!) }); - -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); -``` - -#### ethers - -```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers'; - -const l1 = new JsonRpcProvider(process.env.L1_RPC!); -const l2 = new JsonRpcProvider(process.env.L2_RPC!); -const signer = new Wallet(process.env.PRIVATE_KEY!, l1); - -const client = await createEthersClient({ l1, l2, signer }); -const sdk = createEthersSdk(client); -``` - -### Identical SDK Usage - -Once the adapter is set up, **your application logic is the same**: - -```ts -const quote = await sdk.deposits.quote({ - token: ETH_ADDRESS, - amount: parseEther('0.1'), - to: '0xYourAddress', -}); - -console.log('Total fee:', quote.totalFee.toString()); -``` - -## Conclusion - -The adapter model is designed to give you flexibility without adding complexity. Your choice of adapter is a low-stakes decision that's easy to change later. - -**Ready to start building?** 🚀 - -- [**Go to Quickstart (viem)**](./viem.md) -- [**Go to Quickstart (ethers)**](./ethers.md) diff --git a/docs/src/quickstart/ethers.md b/docs/src/quickstart/ethers.md deleted file mode 100644 index 4fce499..0000000 --- a/docs/src/quickstart/ethers.md +++ /dev/null @@ -1,146 +0,0 @@ -# Quickstart (ethers): ETH Deposit (L1 → L2) - -This guide will get you from zero to a working **ETH deposit from Ethereum to ZKsync (L2)** in minutes using the **ethers** adapter. 🚀 - -You'll set up your environment, write a short script to make a deposit, and run it. - -## 1. Prerequisites - -- You have [Bun](https://bun.sh/) installed. -- You have an L1 wallet (e.g., Sepolia testnet) funded with some ETH to pay for gas and the deposit. - -## 2. Installation & Setup - -First, install the necessary packages. - -```bash -bun install @dutterbutter/zksync-sdk ethers dotenv -``` - -Next, create a `.env` file in your project's root directory to store your private key and RPC endpoints. **Never commit this file to Git.** - -**`.env` file:** - -```env -# Your funded L1 wallet private key (e.g., from MetaMask) -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" -``` - -## 3. The Deposit Script - -The following script will connect to the networks, create a deposit transaction, send it, and wait for it to be confirmed on both L1 and L2. - -Save this code as `deposit-ethers.ts`: - -```ts -import 'dotenv/config'; // Load environment variables from .env -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient } 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 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); - - /* - // OPTIONAL: ADVANCED CONTROL - // The SDK also lets you inspect a transaction before sending it. - // This follows the Mental Model: quote -> prepare -> create. - // Uncomment the code below to see it in action. - - const params = { - token: ETH_ADDRESS, - amount: parseEther('0.001'), - to: account.address, - }; - - // Get a quote for the fees - const quote = await sdk.deposits.quote(params); - console.log('Fee quote:', quote); - - // Prepare the transaction without sending - const plan = await sdk.deposits.prepare(params); - console.log('Transaction plan:', plan); - */ -} - -main().catch((error) => { - console.error('An error occurred:', error); - process.exit(1); -}); -``` - -## 4. Run the Script - -Execute the script using `bun`. - -```bash -bun run deposit-ethers.ts -``` - -You should see output confirming the L1 transaction, the wait periods, and finally the successful L2 verification. - -## 5. Troubleshooting - -- **Insufficient funds on L1:** Make sure your wallet has enough ETH on L1 to cover both the deposit amount (`0.001` ETH) and the L1 gas fees. -- **Invalid `PRIVATE_KEY`:** Ensure it’s a 64-character hex string, prefixed with `0x`. -- **Stuck waiting for L2:** This can take a few minutes. If it takes too long, check that your `L2_RPC_URL` is correct and the network is operational. diff --git a/docs/src/quickstart/index.md b/docs/src/quickstart/index.md deleted file mode 100644 index 49db31b..0000000 --- a/docs/src/quickstart/index.md +++ /dev/null @@ -1,22 +0,0 @@ -# Quickstart - -The Quickstart guides help you get your first ZKsync deposit action running in minutes. -You’ll learn how to install the SDK, connect a client, and perform a deposit. - -## Choose your adapter - -This SDK extends existing Ethereum libraries. Pick the Quickstart that matches your stack: - -- [Quickstart (viem)](viem.md) — for projects already using **viem**. -- [Quickstart (ethers)](ethers.md) — for projects using **ethers v6**. - -## What you’ll do - -Each Quickstart walks you through: - -1. **Install** the adapter package. -2. **Configure** a client or signer. -3. **Run** a deposit (L1 → L2) as a working example. -4. **Track** the status until it’s complete. - -👉 Once you’re set up, continue to the [How-to Guides](../guides/index.md) for more detailed usage. diff --git a/docs/src/quickstart/viem.md b/docs/src/quickstart/viem.md deleted file mode 100644 index 3276dea..0000000 --- a/docs/src/quickstart/viem.md +++ /dev/null @@ -1,144 +0,0 @@ -# Quickstart (viem): ETH Deposit (L1 → L2) - -This guide gets you to a working **ETH deposit from Ethereum to ZKsync (L2)** using the **viem** adapter. - -You’ll set up your environment, write a short script, and run it. - -## 1. Prerequisites - -- You have [Bun](https://bun.sh/) (or Node + tsx) installed. -- You have an **L1 wallet** funded with ETH to cover the deposit amount **and** L1 gas. - -## 2. Installation & Setup - -Install packages: - -```bash -bun install @dutterbutter/zksync-sdk viem dotenv -# or: npm i @dutterbutter/zksync-sdk viem dotenv -``` - -Create an `.env` in your project root (never commit this): - -```env -# 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 -``` - -## 3. The Deposit Script - -Save as `deposit-viem.ts`: - -```ts -import 'dotenv/config'; // Load environment variables from .env -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 your PRIVATE_KEY, L1_RPC_URL, and L2_RPC_URL in a .env file'); - } - - // 1. SET UP CLIENTS AND ACCOUNT - // The SDK needs connections to both L1 and L2 to function. - 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 CLIENT - // The client bundles your viem clients; the SDK surface exposes deposits/withdrawals helpers. - const client = createViemClient({ l1, l2, l1Wallet }); - const sdk = createViemSdk(client); - - const L1balance = await l1.getBalance({ address: account.address }); - const L2balance = await l2.getBalance({ address: account.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: account.address }); - const L2balanceAfter = await l2.getBalance({ address: account.address }); - - console.log('Wallet balance on L1 after:', L1balanceAfter); - console.log('Wallet balance on L2 after:', L2balanceAfter); - - /* - // OPTIONAL: ADVANCED CONTROL - // The SDK also lets you inspect a transaction before sending it. - // This follows the Mental Model: quote -> prepare -> create. - // Uncomment the code below to see it in action. - - const params = { - token: ETH_ADDRESS, - amount: parseEther('0.001'), - to: account.address, - }; - - // Get a quote for the fees - const quote = await sdk.deposits.quote(params); - console.log('Fee quote:', quote); - - // Prepare the transaction without sending - const plan = await sdk.deposits.prepare(params); - console.log('Transaction plan:', plan); - */ -} - -main().catch((error) => { - console.error('An error occurred:', error); - process.exit(1); -}); -``` - -## 4. Run the Script - -```bash -bun run deposit-viem.ts -# or with tsx: -# npx tsx deposit-viem.ts -``` - -You’ll see logs for the L1 transaction, then L2 execution, and a final status snapshot. - -## 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:** May require an L1 `approve()`; `quote()` will surface required steps. diff --git a/docs/src/reference/helpers.md b/docs/src/reference/helpers.md deleted file mode 100644 index 0825271..0000000 --- a/docs/src/reference/helpers.md +++ /dev/null @@ -1,77 +0,0 @@ -# Common Helpers - -Convenience APIs for **addresses, contracts, and token mapping**. -Available under `sdk.helpers` (`ethers` and `viem` adapter). - -## Addresses - -```ts -const addresses = await sdk.helpers.addresses(); -console.log(addresses.bridgehub); -``` - -Resolves and caches core contract addresses (Bridgehub, routers, vaults, core contracts). -Call `client.refresh()` to clear the cache if networks/overrides change. - -## Contracts - -```ts -const contracts = await sdk.helpers.contracts(); -console.log(await contracts.l1AssetRouter.paused()); -``` - -Returns connected `ethers.Contract` instances (`viem` equivalents) for all core contracts. -You can also call individual shortcuts: - -```ts -const router = await sdk.helpers.l1AssetRouter(); -const vault = await sdk.helpers.l1NativeTokenVault(); -const nullifier = await sdk.helpers.l1Nullifier(); -``` - -## Base Token - -```ts -const base = await sdk.helpers.baseToken(); -const baseOther = await sdk.helpers.baseToken(BigInt(300)); -``` - -Reads the **base token** for the current L2 network, or a specific chain id. - -## Token Mapping - -**L1 → L2** - -```ts -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const l2Eth = await sdk.helpers.l2TokenAddress(ETH_ADDRESS); -const l2Usdc = await sdk.helpers.l2TokenAddress('0x...'); -``` - -- ETH maps to the special ETH placeholder on L2. -- If the L1 token is the base token, you get the L2 base-token system address. - -**L2 → L1** - -```ts -const l1Token = await sdk.helpers.l1TokenAddress('0x...L2Token'); -``` - -Maps an L2 token back to its L1 token. - -## Asset ID - -```ts -import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core'; - -const ethId = await sdk.helpers.assetId(ETH_ADDRESS); -const tokenId = await sdk.helpers.assetId('0x...'); -``` - -Fetches the **assetId (bytes32)** for a token. -ETH is handled automatically. - -## Behavior & Notes - -- **Caching:** `addresses()` and `contracts()` results are cached; use `client.refresh()` to reset. diff --git a/docs/src/reference/index.md b/docs/src/reference/index.md deleted file mode 100644 index 3608e3a..0000000 --- a/docs/src/reference/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# Reference - -This section documents the **low-level APIs** exposed by the SDK. -Unlike the high-level flows (`deposits`, `withdrawals`), these helpers give you direct access to ZKsync-specific contracts and RPC methods. - -## What you’ll find here - -- **ZKsync RPC Helpers** - A typed interface around ZKsync `zks_` JSON-RPC methods such as: - - `getBridgehubAddress()` - - `getL2ToL1LogProof()` - - `getReceiptWithL2ToL1()` - -- **Common Helpers** - Utility getters for frequently used contracts and addresses, such as: - - `l1AssetRouter()`, `l1Nullifier()`, `l1NativeTokenVault()` - - `baseToken(chainId)` - - `l1TokenAddress(l2Token)`, `l2TokenAddress(l1Token)` - - `assetId(l1Token)` diff --git a/docs/src/reference/methods.md b/docs/src/reference/methods.md deleted file mode 100644 index 79220c8..0000000 --- a/docs/src/reference/methods.md +++ /dev/null @@ -1,84 +0,0 @@ -# ZKsync `zks_` RPC Helpers - -These helpers expose ZKsync-specific RPC methods through the SDK’s client. -They work the same whether you’re using the **viem** or **ethers** adapter. - -> In all examples below, assume you’ve already created a `client` (via `createViemClient` or `createEthersClient`). -> Calls are identical across adapters: `client.zks.*`. - ---- - -## `getBridgehubAddress()` - -**What it does** -Returns the canonical **Bridgehub** contract address. - -**Example** - -```ts -const bridgehub = await client.zks.getBridgehubAddress(); -console.log('Bridgehub:', bridgehub); // 0x... -``` - -**Returns** -`Address` (EVM address string, `0x…`) - -## `getReceiptWithL2ToL1(txHash)` - -**What it does** -Fetches the transaction receipt for an **L2** tx and includes `l2ToL1Logs` as an array. -This makes it easy to locate L2→L1 messages without guessing the shape. - -**Example** - -```ts -const l2TxHash = '0x...'; // L2 transaction hash -const receipt = await client.zks.getReceiptWithL2ToL1(l2TxHash); - -if (!receipt) { - console.log('Receipt not found yet'); -} else { - console.log('l2ToL1Logs count:', receipt.l2ToL1Logs.length); - // e.g. find the first L1MessageSent-like entry here if you need raw data -} -``` - -**Returns** -`ReceiptWithL2ToL1 | null` - -- Same fields as a normal receipt, plus **`l2ToL1Logs: any[]`** (always present; empty if none). -- `null` when the node does not yet have the receipt. - -## `getL2ToL1LogProof(txHash, index)` - -**What it does** -Fetches the **proof** for an L2→L1 log at a given `index` in the transaction’s messenger logs. -The SDK normalizes the response to a consistent shape. - -**Example** - -```ts -const l2TxHash = '0x...'; -const messengerLogIndex = 0; // whichever log index you intend to finalize - -try { - const proof = await client.zks.getL2ToL1LogProof(l2TxHash, messengerLogIndex); - // proof.id, proof.batchNumber, proof.proof (Hex[]) - console.log('Proof id:', proof.id.toString()); - console.log('Batch number:', proof.batchNumber.toString()); - console.log('Proof length:', proof.proof.length); -} catch (e) { - // If the proof is not yet available, the SDK raises a STATE error with a clear message. - console.error('Proof unavailable yet or RPC error:', e); -} -``` - -**Returns** - -```ts -type ProofNormalized = { - id: bigint; - batchNumber: bigint; - proof: `0x${string}`[]; -}; -``` diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..dd32e77 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,377 @@ +/* ========================= + Base / Tokens / Resets + ========================= */ +html, +body { + overflow-x: hidden; +} + +:root { + /* Brand */ + --zk-blue: #1755f4; + --zk-royal: #0c18ec; + --zk-teal: #13d5d3; + --zk-lime: #bff351; + + /* UI neutrals */ + --white: #ffffff; + --surface: #ffffff; + --ink-0: #05060a; /* dark bg */ + --ink-1: #0b1226; /* heading */ + --ink-2: #334155; /* body */ + --line: rgba(2, 6, 23, 0.1); + + /* CTA teal */ + --teal-700: #0ba3a1; + --teal-600: #0fbebb; +} + +/* ========================= + Hero + ========================= */ +.hero { + position: relative; + padding: 96px 16px; +} + +/* background grid + radial glow */ +.hero--ai { + width: 100vw; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + border-bottom: 1px solid var(--line); + background: + radial-gradient(rgba(23, 85, 244, 0.06) 1px, transparent 1.2px) 0 0 / 18px 18px, + radial-gradient(1200px 700px at 85% -10%, rgba(12, 24, 236, 0.26), transparent 60%), + radial-gradient(1100px 600px at 10% 80%, rgba(19, 213, 211, 0.24), transparent 60%), + radial-gradient(900px 580px at 30% 20%, rgba(239, 109, 242, 0.2), transparent 60%), + radial-gradient(900px 520px at 70% 70%, rgba(253, 64, 44, 0.16), transparent 60%), + radial-gradient(1200px 520px at -10% -20%, rgba(191, 243, 81, 0.18), transparent 60%), + var(--surface); +} +:root.dark .hero--ai { + border-bottom-color: rgba(23, 85, 244, 0.28); + background: + radial-gradient(rgba(23, 85, 244, 0.13) 1px, transparent 1.2px) 0 0 / 18px 18px, + radial-gradient(1250px 720px at 62% -10%, rgba(12, 24, 236, 0.6), transparent 62%), + radial-gradient(1100px 650px at 10% 86%, rgba(19, 213, 211, 0.25), transparent 62%), + radial-gradient(900px 600px at 26% 28%, rgba(239, 109, 242, 0.25), transparent 62%), + radial-gradient(900px 560px at 76% 72%, rgba(253, 64, 44, 0.18), transparent 62%), + radial-gradient(1100px 540px at -10% -20%, rgba(191, 243, 81, 0.16), transparent 62%), + var(--ink-0); +} + +/* grid split */ +.hero--split .hero-inner { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 40px; + align-items: center; + padding: 0 24px; +} +.promo-left { + display: flex; + flex-direction: column; + gap: 18px; + align-items: flex-start; +} +.promo-right { + display: flex; + justify-content: flex-end; +} + +/* ========================= + Pill + ========================= */ +.pill-tag { + align-self: flex-start; + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + border-radius: 999px; + font-size: 0.95rem; + font-weight: 600; + color: var(--ink-1); + border: 1px solid rgba(191, 243, 81, 0.45); + background: color-mix(in srgb, var(--zk-blue) 8%, #fff); +} +.pill-tag--ring { + border: 1px solid transparent; + background: + linear-gradient(#fff, #fff) padding-box, + conic-gradient(#bff351, #1755f4, #ef6df2, #fd402c, #13d5d3, #bff351) border-box; +} +:root.dark .pill-tag { + color: #f1f5ff; + background: linear-gradient(180deg, rgba(12, 24, 236, 0.32), rgba(12, 24, 236, 0.16)); + border-color: rgba(191, 243, 81, 0.6); +} +:root.dark .pill-tag--ring { + background: + linear-gradient(#0c1322, #0c1322) padding-box, + conic-gradient(#bff351, #1755f4, #ef6df2, #fd402c, #13d5d3, #bff351) border-box; + border: 1px solid transparent; +} + +/* ========================= + Headline & Subtitle + ========================= */ +.promo-title { + font-size: clamp(2.6rem, 6vw, 4.4rem); + line-height: 0.95; + letter-spacing: -0.02em; + font-weight: 800; + margin: 0; + max-width: 12ch; + color: var(--ink-1); +} +.promo-title .zk-strong { + color: var(--zk-blue); +} +:root.dark .promo-title { + color: var(--zk-blue); +} +:root.dark .promo-title .zk-strong { + color: var(--white); +} + +.hero-subtitle { + max-width: 680px; + font-size: clamp(1.05rem, 2vw, 1.25rem); + line-height: 1.65; + margin: 6px 0 4px; + color: var(--ink-2); +} +:root.dark .hero-subtitle { + color: #a7b1d6; +} + +/* ========================= + CTAs + ========================= */ +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 6px; +} + +.hero-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 14px 22px; + border-radius: 12px; + font-weight: 600; + text-decoration: none; + color: #fff; + background: var(--zk-blue); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); + transition: + transform 0.06s ease, + box-shadow 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease; +} +.hero-cta:hover { + transform: translateY(-1px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12); +} + +.hero-cta.ghost { + background: #fff; + color: var(--ink-1); + border: 1px solid var(--line); + box-shadow: none; +} +.hero-cta.ghost:hover { + background: #f8fafc; + border-color: rgba(2, 6, 23, 0.14); +} + +.hero-cta.teal { + background: linear-gradient(90deg, var(--teal-700), var(--teal-600)); + color: #fff; + border: 1px solid rgba(19, 213, 211, 0.35); +} + +/* dark */ +:root.dark .hero-cta { + background: linear-gradient(90deg, #1755f4, #0c18ec); + color: #fff; + box-shadow: 0 8px 24px rgba(23, 85, 244, 0.35); +} +:root.dark .hero-cta.teal { + background: linear-gradient(90deg, var(--teal-700), var(--teal-600)); + border: 1px solid rgba(19, 213, 211, 0.45); + box-shadow: 0 10px 28px rgba(19, 213, 211, 0.32); +} +:root.dark .hero-cta.ghost { + background: #fff; + color: #0b1226; + border: 1px solid rgba(2, 6, 23, 0.28); +} +:root.dark .hero-cta.ghost:hover { + background: #f8fafc; + border-color: rgba(2, 6, 23, 0.4); +} + +/* ========================= + Installer card + ========================= */ +.dev-card { + width: 100%; + max-width: 520px; + border-radius: 16px; + padding: 16px; + border: 1px solid var(--line); + background: #fff; + box-shadow: 0 8px 26px rgba(2, 6, 23, 0.06); +} +.dev-label { + font-size: 0.9rem; + font-weight: 600; + color: #64748b; + margin-bottom: 8px; +} +.dev-footnote { + margin-top: 10px; + font-size: 0.85rem; + color: #94a3b8; +} + +/* single-border look for code block */ +.dev-card pre, +.dev-card pre[class*='language-'], +.dev-card .shiki, +.dev-card .code-block, +.dev-card pre code { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} +.dev-card pre { + margin: 0 !important; + padding: 14px 16px !important; + border-radius: 10px !important; +} + +/* gradient ring variant */ +.dev-card--gradient { + position: relative; + border: 1px solid transparent; + border-radius: 16px; + background: + linear-gradient(#fff, #fff) padding-box, + linear-gradient(135deg, #1755f4, #ef6df2, #fd402c, #13d5d3) border-box; + box-shadow: 0 12px 32px rgba(2, 6, 23, 0.1); +} + +/* dark */ +:root.dark .dev-card { + background: linear-gradient(180deg, rgba(12, 24, 236, 0.18), rgba(12, 16, 28, 0.3)); + border-color: rgba(232, 237, 255, 0.16); + box-shadow: + 0 14px 40px rgba(0, 0, 0, 0.55), + 0 0 0 1px rgba(5, 12, 255, 0.06) inset; +} +:root.dark .dev-card--gradient { + background: + linear-gradient(#0c1322, #0c1322) padding-box, + linear-gradient(135deg, #1755f4, #ef6df2, #fd402c, #13d5d3) border-box; + border: 1px solid transparent; +} +:root.dark .dev-card pre code { + color: #f3faff; +} + +/* ========================= + Sections & responsive + ========================= */ +.lp-container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 24px 56px; +} +.lp-container > h2 { + font-size: clamp(1.25rem, 2vw, 1.5rem); + font-weight: 600; + margin: 0 0 16px; +} + +@media (max-width: 960px) { + .hero--split .hero-inner { + grid-template-columns: 1fr; + gap: 28px; + } + .promo-right { + justify-content: flex-start; + } +} +@media (min-width: 1280px) { + .hero--split .hero-inner { + gap: 44px; + } + .promo-right { + transform: translateY(6px); + } +} + +/* ========================= + Top bleed for non-hero pages + ========================= */ +:root { + --zk-bleed-height: min(38vh, 560px); +} + +body { + position: relative; + background: var(--surface); +} +:root.dark body { + background: var(--ink-0); +} + +/* only on pages WITHOUT the hero */ +body:not(.has-hero)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: var(--zk-bleed-height); + pointer-events: none; + z-index: 0; + background: + radial-gradient(rgba(23, 85, 244, 0.06) 1px, transparent 1.2px) 0 0 / 18px 18px, + radial-gradient(1200px 700px at 85% -10%, rgba(12, 24, 236, 0.26), transparent 60%), + radial-gradient(1100px 600px at 10% 80%, rgba(19, 213, 211, 0.24), transparent 60%), + radial-gradient(900px 580px at 30% 20%, rgba(239, 109, 242, 0.2), transparent 60%), + radial-gradient(900px 520px at 70% 70%, rgba(253, 64, 44, 0.16), transparent 60%), + radial-gradient(1200px 520px at -10% -20%, rgba(191, 243, 81, 0.18), transparent 60%), + transparent; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 58%, rgba(0, 0, 0, 0)); + -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 58%, rgba(0, 0, 0, 0)); + box-shadow: inset 0 -1px 0 var(--line); +} + +:root.dark body:not(.has-hero)::before { + background: + radial-gradient(rgba(23, 85, 244, 0.13) 1px, transparent 1.2px) 0 0 / 18px 18px, + radial-gradient(1250px 720px at 62% -10%, rgba(12, 24, 236, 0.6), transparent 62%), + radial-gradient(1100px 650px at 10% 86%, rgba(19, 213, 211, 0.25), transparent 62%), + radial-gradient(900px 600px at 26% 28%, rgba(239, 109, 242, 0.25), transparent 62%), + radial-gradient(900px 560px at 76% 72%, rgba(253, 64, 44, 0.18), transparent 62%), + radial-gradient(1100px 540px at -10% -20%, rgba(191, 243, 81, 0.16), transparent 62%), + transparent; +} + +/* ensure content sits above the pseudo */ +body > * { + position: relative; +} diff --git a/src/adapters/ethers/resources/withdrawals/routes/eth.ts b/src/adapters/ethers/resources/withdrawals/routes/eth.ts index 37a29ff..fd0a6de 100644 --- a/src/adapters/ethers/resources/withdrawals/routes/eth.ts +++ b/src/adapters/ethers/resources/withdrawals/routes/eth.ts @@ -16,12 +16,7 @@ export function routeEthBase(): WithdrawRouteStrategy { async build(p, ctx) { const steps: Array> = []; - const base = new Contract( - L2_BASE_TOKEN_ADDRESS, - - new Interface(IBaseTokenABI), - ctx.client.l2, - ); + const base = new Contract(L2_BASE_TOKEN_ADDRESS, new Interface(IBaseTokenABI), ctx.client.l2); const toL1 = p.to ?? ctx.sender; const data = await wrapAs( diff --git a/src/adapters/ethers/sdk.ts b/src/adapters/ethers/sdk.ts index d3d0c0f..01c01ee 100644 --- a/src/adapters/ethers/sdk.ts +++ b/src/adapters/ethers/sdk.ts @@ -13,13 +13,36 @@ import { type Address, type Hex } from '../../core/types'; import { isAddressEq } from '../../core/utils/addr'; import { L2_BASE_TOKEN_ADDRESS, ETH_ADDRESS, FORMAL_ETH_ADDRESS } from '../../core/constants'; -// SDK interface, combining deposits, withdrawals, and helpers +/** + * @summary The main entry point for interacting with the ZKsync network using the Ethers.js adapter. + * @description This SDK object provides access to all major functionalities, including deposits, + * withdrawals, and various utility helpers for address and contract resolution. + */ export interface EthersSdk { + /** + * @summary Provides methods for depositing assets from L1 to L2. + * @see DepositsResourceType for a full list of methods. + */ deposits: DepositsResourceType; + /** + * @summary Provides methods for withdrawing assets from L2 to L1. + * @see WithdrawalsResourceType for a full list of methods. + */ withdrawals: WithdrawalsResourceType; + /** + * @summary A collection of utility functions for common tasks like resolving addresses, + * fetching contracts, and converting token addresses. + */ helpers: { - // addresses & contracts + /** + * @summary Retrieves the resolved L1 and L2 contract addresses used by the SDK. + * @returns A Promise that resolves to the set of contract addresses. + */ addresses(): Promise; + /** + * @summary Retrieves the Ethers.js Contract instances for all SDK-related contracts. + * @returns A Promise that resolves to an object containing all relevant contract instances. + */ contracts(): Promise<{ bridgehub: Contract; l1AssetRouter: Contract; @@ -30,17 +53,62 @@ export interface EthersSdk { l2BaseTokenSystem: Contract; }>; - // common getters + /** + * @summary Gets the L1 Asset Router contract instance. + * @returns A Promise resolving to the `Contract` instance. + */ l1AssetRouter(): Promise; + /** + * @summary Gets the L1 Native Token Vault contract instance. + * @returns A Promise resolving to the `Contract` instance. + */ l1NativeTokenVault(): Promise; + /** + * @summary Gets the L1 Nullifier contract instance. + * @returns A Promise resolving to the `Contract` instance. + */ l1Nullifier(): Promise; + /** + * @summary Retrieves the L1 address of the base token for a given L2 chain. + * @param chainId The L2 chain ID. If not provided, it's fetched from the L2 provider. + * @returns A Promise resolving to the base token's L1 address. + */ baseToken(chainId?: bigint): Promise
; + /** + * @summary Gets the corresponding L2 token address for a given L1 token. + * @notice This method correctly handles special cases for ETH and the official base token. + * @param l1Token The address of the token on L1. + * @returns A Promise resolving to the corresponding token address on L2. + * @example + * ```typescript + * const l1EthAddress = '0x0000000000000000000000000000000000000000'; + * const l2EthAddress = await sdk.helpers.l2TokenAddress(l1EthAddress); + * console.log(l2EthAddress); // 0x000000000000000000000000000000000000800a + * ``` + */ l2TokenAddress(l1Token: Address): Promise
; + /** + * @summary Gets the corresponding L1 token address for a given L2 token. + * @notice This method correctly handles the placeholder address for ETH on L2. + * @param l2Token The address of the token on L2. + * @returns A Promise resolving to the corresponding token address on L1. + */ l1TokenAddress(l2Token: Address): Promise
; + /** + * @summary Calculates the unique asset identifier (`bytes32`) for a given L1 token. + * @param l1Token The address of the token on L1. + * @returns A Promise that resolves to the `bytes32` asset ID as a hex string. + */ assetId(l1Token: Address): Promise; }; } +/** + * @summary Creates an instance of the EthersSdk. + * @param client An instance of `EthersClient` used for communication with the network. + * @returns A fully configured `EthersSdk` instance. + * @internal + */ export function createEthersSdk(client: EthersClient): EthersSdk { return { deposits: createDepositsResource(client), diff --git a/src/core/types/errors.ts b/src/core/types/errors.ts index c231330..0762bae 100644 --- a/src/core/types/errors.ts +++ b/src/core/types/errors.ts @@ -53,6 +53,7 @@ export interface ErrorEnvelope { cause?: unknown; } +// TODO: move to errors/ /** Error class. * Represents an error that occurs within the ZKsync SDK. * It encapsulates an ErrorEnvelope which provides detailed information about the error, diff --git a/typedoc.json b/typedoc.json index 3059ea8..28e6436 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,8 +1,8 @@ { - "entryPoints": ["packages/core/src/index.ts", "packages/ethers/src/index.ts"], + "entryPoints": ["src/adapters/ethers/sdk.ts"], "entryPointStrategy": "resolve", "tsconfig": "tsconfig.build.json", - "out": "docs/src/api", + "out": "docs/src/api-reference", "excludePrivate": true, "excludeExternals": false, "readme": "none",