diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9607262..f8b4b1f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -45,6 +45,9 @@ Before submitting a PR, please make sure: ``` - Update or add docs/examples if needed. + For docs, make sure to import any code examples from tests inside `docs/snippets`. + Use `ANCHOR` comments with unique tags to specify a code block within a test to import into a markdown file. + For more information, check out the [mdbook docs](https://rust-lang.github.io/mdBook/format/mdbook.html#including-files). - Link the related issue (if any). ## πŸ€– AI-Assisted Development diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 149d9fc..5b4b58d 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -29,7 +29,7 @@ jobs: - name: Run unit tests w/ coverage run: | - bun test + bun test src test -f coverage/lcov.info || (echo "lcov not found for unit tests" && exit 1) mv coverage/lcov.info lcov.unit.info @@ -165,3 +165,48 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + docs-examples: + name: docs-examples πŸ“‘ + permissions: + contents: read + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + suite: + - "docs/snippets/core" + - "docs/snippets/ethers" + - "docs/snippets/viem" + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install build deps + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends lcov + + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@8b0419c685ef46cb79ec93fbdc131174afceb730 # v1.6.0 + with: + version: v1.5.1 + + - name: Run ZKsync OS (L1-L2) + uses: dutterbutter/zksync-server-action@e0401b155b1fa0f4f3629d569db3ae7094383729 # main + + - name: Setup Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: '1.3.5' + + - name: Install deps + run: bun install + + - name: Build + run: bun run build + + - name: Run docs examples tests + env: + L1_RPC: http://localhost:8545 + L2_RPC: http://localhost:3050 + PRIVATE_KEY: '0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110' + run: | + bun test ${{ matrix.suite }} --timeout 100000 \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml index 1e197e4..912960b 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -12,6 +12,8 @@ build-dir = "book" # output folder (docs/book) command = "mdbook-admonish" assets_version = "3.0.3" # do not edit: managed by `mdbook-admonish install` +[preprocessor.yapp] + [output.html] additional-css = ["./mdbook-admonish.css"] default-theme = "light" diff --git a/docs/snippets/core/errors.test.ts b/docs/snippets/core/errors.test.ts new file mode 100644 index 0000000..6140aeb --- /dev/null +++ b/docs/snippets/core/errors.test.ts @@ -0,0 +1,144 @@ +// ANCHOR: error-import +import { isZKsyncError } from '../../../src/core/types/errors'; +// ANCHOR_END: error-import + +import { type ErrorEnvelope as Envelope, Resource, ErrorType } from '../../../src/core/types/errors'; +import { Account, createPublicClient, createWalletClient, http, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk, type ViemSdk } from '../../../src/adapters/viem'; +import { beforeAll, describe, it } from 'bun:test'; +import { l1Chain, l2Chain } from '../viem/chains'; +import { ETH_ADDRESS } from '../../../src/core'; +import type { Exact } from "./types"; + +// ANCHOR: envelope-type +interface ErrorEnvelope { + /** Resource surface that raised the error. */ + resource: Resource; + /** SDK operation, e.g. 'withdrawals.finalize' */ + operation: string; + /** Broad category */ + type: ErrorType; + /** Human-readable, stable message for developers. */ + message: string; + + /** Optional detail that adapters may enrich (reverts, extra context) */ + context?: Record; + + /** If the error is a contract revert, adapters add decoded info here. */ + revert?: { + /** 4-byte selector as 0x…8 hex */ + selector: `0x${string}`; + /** Decoded error name when available (e.g. 'InvalidProof') */ + name?: string; + /** Decoded args (ethers/viem output), when available */ + args?: unknown[]; + /** Optional adapter-known labels */ + contract?: string; + /** Optional adapter-known function name */ + fn?: string; + }; + + /** Original thrown error */ + cause?: unknown; +} +// ANCHOR_END: envelope-type + +describe('checks rpc docs examples', () => { + +let sdk: ViemSdk; +let account: Account; +let params: any; + +beforeAll(() => { + 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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); + const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + + const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); + sdk = createViemSdk(client); + + params = { + amount: parseEther('0.01'), + to: account.address, + token: ETH_ADDRESS, +} as const; +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the error types are updated', async () => { + const _envelopeType: Exact = true; +}); + +it('shows how to handle errors', async () => { +// ANCHOR: zksync-error +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; + + switch (type) { + case 'VALIDATION': + case 'STATE': + // user/action fixable (bad input, not-ready, etc.) + break; + case 'EXECUTION': + case 'RPC': + // network/tx/provider issues + break; + } + + console.error(JSON.stringify(err.toJSON())); // structured log + } else { + throw e; // non-SDK error + } +} +// ANCHOR_END: zksync-error +}) + +it('handles a withdrawal error', async () => { +// ANCHOR: try-create +const res = await sdk.withdrawals.tryCreate(params); +if (!res.ok) { + if (isZKsyncError(res.error)) { + // res.error is a ZKsyncError + console.warn(res.error.envelope.message, res.error.envelope.operation); + } else { + throw new Error("Unkown error"); + } +} else { + console.log('l2TxHash', res.value.l2TxHash); +} +// ANCHOR_END: try-create + +if(!res.ok) throw new Error("response not ok"); +const l2TxHash = res.value.l2TxHash; + +// ANCHOR: revert-details +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' + } +} +// ANCHOR_END: revert-details +}) + +it('handles a deposit error', async () => { +// ANCHOR: envelope-error +const res = await sdk.deposits.tryCreate(params); +if (!res.ok) { + if (isZKsyncError(res.error)) console.error(res.error.envelope); // structured envelope +} +// ANCHOR_END: envelope-error +}) + +}); \ No newline at end of file diff --git a/docs/snippets/core/rpc.test.ts b/docs/snippets/core/rpc.test.ts new file mode 100644 index 0000000..c7d042f --- /dev/null +++ b/docs/snippets/core/rpc.test.ts @@ -0,0 +1,133 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, type ViemClient } from '../../../src/adapters/viem'; +import { Address, Hex, type ZksRpc as ZksType } from '../../../src/core'; +import { GenesisContractDeployment, GenesisInput as GenesisType, GenesisStorageEntry, L2ToL1Log, ProofNormalized as ProofN, ReceiptWithL2ToL1 as RWithLog, BlockMetadata as MetadataType } from '../../../src/core/rpc/types'; + +import { l1Chain, l2Chain } from '../viem/chains'; +import type { Exact } from "./types"; + +// ANCHOR: zks-rpc +export interface ZksRpc { + // Fetches the Bridgehub contract address. + getBridgehubAddress(): Promise
; + // Fetches the Bytecode Supplier contract address. + getBytecodeSupplierAddress(): Promise
; + // Fetches a proof for an L2β†’L1 log emitted in the given transaction. + getL2ToL1LogProof(txHash: Hex, index: number): Promise; + // Fetches the transaction receipt, including the `l2ToL1Logs` field. + getReceiptWithL2ToL1(txHash: Hex): Promise; + // Fetches block metadata for the given block number. + getBlockMetadataByNumber(blockNumber: number): Promise; + // Fetches the genesis configuration returned by `zks_getGenesis`. + getGenesis(): Promise; +} +// ANCHOR_END: zks-rpc + +// ANCHOR: proof-receipt-type +type ProofNormalized = { + id: bigint; + batchNumber: bigint; + proof: Hex[]; + root: Hex; +}; + +type ReceiptWithL2ToL1 = { + transactionIndex: Hex; + transactionHash?: Hex; + status?: string | number; + blockNumber?: string | number; + logs?: Array<{ + address: Address; + topics: Hex[]; + data: Hex; + transactionHash: Hex; + }>; + // ZKsync-specific field + l2ToL1Logs?: L2ToL1Log[]; +}; +// ANCHOR_END: proof-receipt-type + +// ANCHOR: genesis-type +export type GenesisInput = { + initialContracts: GenesisContractDeployment[]; + additionalStorage: GenesisStorageEntry[]; + executionVersion: number; + genesisRoot: Hex; +}; +// ANCHOR_END: genesis-type + +// ANCHOR: metadata-type +type BlockMetadata = { + pubdataPricePerByte: bigint; + nativePrice: bigint; + executionVersion: number; +}; +// ANCHOR_END: metadata-type + +describe('checks rpc docs examples', () => { + +let client: ViemClient; + +beforeAll(() => { + 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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); + const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + + client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the zks rpc types are updated', async () => { + const _rpcType: Exact = true; + const _proofType: Exact = true; + const _receiptType: Exact = true; + const _genesisType: Exact = true; + const _metadataType: Exact = true; +}); + +it('tries to get the bridehub address', async () => { +// ANCHOR: bridgehub-address +const addr = await client.zks.getBridgehubAddress(); +// ANCHOR_END: bridgehub-address +expect(addr).toContain("0x"); +}); + +it('tries to get the genesis', async () => { +// ANCHOR: genesis-method +const genesis = await client.zks.getGenesis(); + +for (const contract of genesis.initialContracts) { + console.log('Contract at', contract.address, 'with bytecode', contract.bytecode); +} + +console.log('Execution version:', genesis.executionVersion); +console.log('Genesis root:', genesis.genesisRoot); +// ANCHOR_END: genesis-method +expect(genesis.initialContracts).toBeArray(); +}); + +it('tries to get the bytecode supplier', async () => { +// ANCHOR: bytecode-supplier +const addr = await client.zks.getBytecodeSupplierAddress(); +// ANCHOR_END: bytecode-supplier +expect(addr).toContain("0x"); +}); + +it('tries to get metadata for a block', async () => { +// ANCHOR: block-metadata +const meta = await client.zks.getBlockMetadataByNumber(2); +if (meta) { + console.log(meta.pubdataPricePerByte, meta.nativePrice, meta.executionVersion); +} +// ANCHOR_END: block-metadata +expect(meta?.executionVersion).toBeNumber(); +}); + +}); diff --git a/docs/snippets/core/types.ts b/docs/snippets/core/types.ts new file mode 100644 index 0000000..826a29d --- /dev/null +++ b/docs/snippets/core/types.ts @@ -0,0 +1,8 @@ +export type Exact = + (() => T extends A ? 1 : 2) extends + (() => T extends B ? 1 : 2) + ? (() => T extends B ? 1 : 2) extends + (() => T extends A ? 1 : 2) + ? true + : false + : false; \ No newline at end of file diff --git a/docs/snippets/ethers/deposit-erc20.ts b/docs/snippets/ethers/deposit-erc20.ts deleted file mode 100644 index 0c549b2..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 '@matterlabs/zksync-js/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/guides/deposit-eth-guide.test.ts similarity index 56% rename from docs/snippets/ethers/deposit-eth.ts rename to docs/snippets/ethers/guides/deposit-eth-guide.test.ts index b9d76d5..da42551 100644 --- a/docs/snippets/ethers/deposit-eth.ts +++ b/docs/snippets/ethers/guides/deposit-eth-guide.test.ts @@ -1,12 +1,24 @@ -// examples/deposit-eth.ts +import { describe, it } from 'bun:test'; + +// ANCHOR: imports import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/core'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +import { ETH_ADDRESS } from '../../../../src/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 || ''; +// ANCHOR_END: imports + +describe('ethers deposit ETH guide', () => { + +it('deposits some ETH', async () => { + await main(); +}); +}); + +// ANCHOR: main async function main() { if (!PRIVATE_KEY) { throw new Error('Set your PRIVATE_KEY in the .env file'); @@ -21,10 +33,10 @@ async function main() { const balanceL2 = await l2.getBalance(signer.address); console.log('L2 balance:', balanceL2.toString()); - const client = await createEthersClient({ l1, l2, signer }); + const client = createEthersClient({ l1, l2, signer }); const sdk = createEthersSdk(client); - const me = (await signer.getAddress()); + const me = (await signer.getAddress()) as `0x${string}`; const params = { amount: parseEther('.01'), // 0.01 ETH to: me, @@ -37,21 +49,30 @@ async function main() { } as const; // Quote + // ANCHOR: quote const quote = await sdk.deposits.quote(params); + // ANCHOR_END: quote console.log('QUOTE response: ', quote); - const prepare = await sdk.deposits.prepare(params); - console.log('PREPARE response: ', prepare); + // ANCHOR: prepare + const plan = await sdk.deposits.prepare(params); + // ANCHOR_END: prepare + console.log('PREPARE response: ', plan); // Create (prepare + send) - const create = await sdk.deposits.create(params); - console.log('CREATE response: ', create); + // ANCHOR: create + const handle = await sdk.deposits.create(params); + // ANCHOR_END: create + console.log('CREATE response: ', handle); - const status = await sdk.deposits.status(create); + // ANCHOR: status + const status = await sdk.deposits.status(handle); /* input can be handle or l1TxHash */ + // status.phase: 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' + // ANCHOR_END: status console.log('STATUS response: ', status); // Wait (for now, L1 inclusion) - const receipt = await sdk.deposits.wait(create, { for: 'l1' }); + const receipt = await sdk.deposits.wait(handle, { for: 'l1' }); console.log( 'Included at block:', receipt?.blockNumber, @@ -61,11 +82,11 @@ async function main() { receipt?.hash, ); - const status2 = await sdk.deposits.status(create); + const status2 = await sdk.deposits.status(handle); console.log('STATUS2 response: ', status2); // Wait (for now, L2 inclusion) - const l2Receipt = await sdk.deposits.wait(create, { for: 'l2' }); + const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); console.log( 'Included at block:', l2Receipt?.blockNumber, @@ -75,8 +96,4 @@ async function main() { l2Receipt?.hash, ); } - -main().catch((e) => { - console.error(e); - process.exit(1); -}); +// ANCHOR_END: main \ No newline at end of file diff --git a/docs/snippets/ethers/guides/withdrawals-eth-guide.test.ts b/docs/snippets/ethers/guides/withdrawals-eth-guide.test.ts new file mode 100644 index 0000000..f363304 --- /dev/null +++ b/docs/snippets/ethers/guides/withdrawals-eth-guide.test.ts @@ -0,0 +1,170 @@ +import { beforeAll, describe, it } from 'bun:test'; + +import type { EthersSdk } from '../../../../src/adapters/ethers'; + +// ANCHOR: imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +import { ETH_ADDRESS } from '../../../../src/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 || ''; +// ANCHOR_END: imports + +describe('ethers withdraw ETH guide', () => { + +let sdk: EthersSdk; +let signer: Wallet; + +beforeAll(async () => { +// deposit some ETH first + const l1 = new JsonRpcProvider(process.env.L1_RPC!); + const l2 = new JsonRpcProvider(process.env.L2_RPC!); + signer = new Wallet(process.env.PRIVATE_KEY!, l1); + + const client = createEthersClient({ l1, l2, signer }); + sdk = createEthersSdk(client); + + const params = { + amount: parseEther('0.2'), + to: await signer.getAddress() as `0x${string}`, + token: ETH_ADDRESS, + } as const; + + const handle = await sdk.deposits.create(params); + await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 +}) + +it('withdraws some ETH with main guide', async () => { + await main(); +}); + +it('withdraws some ETH with alt methods', async () => { + await altMethods(sdk, signer); +}); + +}); + +// ANCHOR: main + +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()) as `0x${string}`; + + // Withdraw params (ETH) + const params = { + token: ETH_ADDRESS, + amount: parseEther('0.01'), // 0.01 ETH + to: me, + // l2GasLimit: 300_000n, + } as const; + + // Quote (dry-run only) + // ANCHOR: quote + const quote = await sdk.withdrawals.quote(params); + // ANCHOR_END: quote + console.log('QUOTE: ', quote); + + // ANCHOR: prepare + const plan = await sdk.withdrawals.prepare(params); + // ANCHOR_END: prepare + console.log('PREPARE: ', plan); + + // ANCHOR: create + const handle = await sdk.withdrawals.create(params); + // ANCHOR_END: create + console.log('CREATE:', handle); + + // Quick status check + // ANCHOR: status + const s = await sdk.withdrawals.status(handle.l2TxHash); /* input can be handle or l2TxHash */ +// s.phase: 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +// ANCHOR_END: status + console.log('STATUS (initial):', s.phase); + + // wait for L2 inclusion + // ANCHOR: wait-for-l2 + const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); + // ANCHOR_END: wait-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(handle.l2TxHash)); + + // finalize on L1 + // Use tryFinalize to avoid throwing in an example script + // ANCHOR: wait-for-ready + await sdk.withdrawals.wait(handle.l2TxHash, { for: 'ready' }); + // ANCHOR_END: wait-for-ready + console.log('STATUS (ready):', await sdk.withdrawals.status(handle.l2TxHash)); + + const fin = await sdk.withdrawals.tryFinalize(handle.l2TxHash); + console.log('TRY FINALIZE: ', fin); + + const l1Receipt = await sdk.withdrawals.wait(handle.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).'); + } +} +// ANCHOR_END: main + +async function altMethods(sdk: EthersSdk, signer: Wallet){ + const me = (await signer.getAddress()) as `0x${string}`; + + const params = { + token: ETH_ADDRESS, + amount: parseEther('0.01'), + to: me, + // l2GasLimit: 300_000n, + } as const; + + const handle = await sdk.withdrawals.create(params); + await sdk.withdrawals.wait(handle, { for: 'ready' }); + +// ANCHOR: wfinalize +const result = await sdk.withdrawals.finalize(handle.l2TxHash); +console.log('Finalization result:', result); +// ANCHOR_END: wfinalize + + // ANCHOR: try-catch-create + try { + const handle = await sdk.withdrawals.create(params); +} catch (e) { + // normalized error envelope (type, operation, message, context, optional revert) +} +// ANCHOR_END: try-catch-create + +// ANCHOR: tryCreate +const r = await sdk.withdrawals.tryCreate(params); + +if (!r.ok) { + console.error('Withdrawal failed:', r.error); +} else { + const handle = r.value; + await sdk.withdrawals.wait(handle, { for: 'ready' }); + 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?.hash); + } +} +// ANCHOR_END: tryCreate + +} \ No newline at end of file diff --git a/docs/snippets/ethers/overview/adapter-basic.test.ts b/docs/snippets/ethers/overview/adapter-basic.test.ts new file mode 100644 index 0000000..792e567 --- /dev/null +++ b/docs/snippets/ethers/overview/adapter-basic.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from 'bun:test'; + +// ANCHOR: ethers-basic-imports +import { JsonRpcProvider, Wallet } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +// ANCHOR_END: ethers-basic-imports + +describe('ethers basic setup', () => { +it('inits a basic ethers adapter and creates a deposit', async () => { +// ANCHOR: init-ethers-adapter +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// ANCHOR_END: init-ethers-adapter +}); + +}); diff --git a/docs/snippets/ethers/overview/adapter.test.ts b/docs/snippets/ethers/overview/adapter.test.ts new file mode 100644 index 0000000..1993e1c --- /dev/null +++ b/docs/snippets/ethers/overview/adapter.test.ts @@ -0,0 +1,82 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: ethers-adapter-imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +import { ETH_ADDRESS } from '../../../../src/core'; +// ANCHOR_END: ethers-adapter-imports +import type { EthersSdk } from '../../../../src/adapters/ethers'; + +describe('ethers adapter setup', () => { + +let sdk: EthersSdk; +let signer: Wallet; +let sharedParams: any; + +beforeAll(async() => { +const l1 = new JsonRpcProvider(process.env.L1_RPC!); +const l2 = new JsonRpcProvider(process.env.L2_RPC!); +signer = new Wallet(process.env.PRIVATE_KEY!, l1); + +const client = createEthersClient({ l1, l2, signer }); +sdk = createEthersSdk(client); +sharedParams = { + amount: parseEther('0.1'), + to: await signer.getAddress() as `0x${string}`, + token: ETH_ADDRESS, +} as const; +}) + +it('creates a deposit', async () => { +// ANCHOR: ethers-deposit +const params = { + amount: parseEther('0.1'), + to: await signer.getAddress() as `0x${string}`, + token: ETH_ADDRESS, +} as const; + +const handle = await sdk.deposits.create(params); +await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 +// ANCHOR_END: ethers-deposit +}); + +it('shows mental model', async () => { + const params = sharedParams; +// ANCHOR: mental-model +// 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); +} +// ANCHOR_END: mental-model +expect(result.ok).toEqual(true); +}); + +it('shows simple flow', async () => { + const params = sharedParams; +// ANCHOR: simple-flow +// 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!'); +// ANCHOR_END: simple-flow +expect(receipt?.hash).toContain("0x"); +}); + +}); diff --git a/docs/snippets/ethers/quickstart/quickstart.test.ts b/docs/snippets/ethers/quickstart/quickstart.test.ts new file mode 100644 index 0000000..4044bce --- /dev/null +++ b/docs/snippets/ethers/quickstart/quickstart.test.ts @@ -0,0 +1,106 @@ +import { describe, it } from 'bun:test'; + +// ANCHOR: quickstart-imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +import { ETH_ADDRESS } from '../../../../src/core'; + +const PRIVATE_KEY = process.env.PRIVATE_KEY; +const L1_RPC_URL = process.env.L1_RPC; +const L2_RPC_URL = process.env.L2_RPC; +// ANCHOR_END: quickstart-imports + +describe('viem quickstart', () => { + +it('deposits some ETH and checks balances', async () => { + await main(); +}); + +}); + +// ANCHOR: quickstart-main + +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 = createEthersClient({ + l1: l1Provider, + l2: l2Provider, + signer, + }); + const sdk = createEthersSdk(client); + + const L1balance = await l1Provider.getBalance(signer.address); + const L2balance = await l2Provider.getBalance(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: signer.address as `0x${string}`, + }); + + 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 l1Provider.getBalance(signer.address); + const L2balanceAfter = await l2Provider.getBalance(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, + // Optional: pin gas fees instead of using provider estimates + // l1TxOverrides: { + // gasLimit: 280_000n, + // maxFeePerGas: parseEther('0.00000002'), // 20 gwei + // maxPriorityFeePerGas: parseEther('0.000000002'), // 2 gwei + // }, + }; + + // 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); + */ +} +// ANCHOR_END: quickstart-main \ No newline at end of file diff --git a/docs/snippets/ethers/reference/client.test.ts b/docs/snippets/ethers/reference/client.test.ts new file mode 100644 index 0000000..35deb81 --- /dev/null +++ b/docs/snippets/ethers/reference/client.test.ts @@ -0,0 +1,95 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: ethers-import +import { JsonRpcProvider, Wallet } from 'ethers'; +// ANCHOR_END: ethers-import +// ANCHOR: client-import +import { createEthersClient } from '../../../../src/adapters/ethers'; +// ANCHOR_END: client-import +import type { Address } from 'viem'; +import type { EthersClient, ResolvedAddresses as RAddrs } from '../../../../src/adapters/ethers/client'; +import type { Exact } from "../../core/types"; + +// ANCHOR: resolved-type +type ResolvedAddresses = { + bridgehub: Address; + l1AssetRouter: Address; + l1Nullifier: Address; + l1NativeTokenVault: Address; + l2AssetRouter: Address; + l2NativeTokenVault: Address; + l2BaseTokenSystem: Address; +}; +// ANCHOR_END: resolved-type + +describe('ethers client', () => { + +let ethersClient: EthersClient; + +beforeAll(async () => { +// ANCHOR: init-client +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 = createEthersClient({ l1, l2, signer }); + +// Resolve core addresses (cached) +const addrs = await client.ensureAddresses(); + +// Connected contracts +const { bridgehub, l1AssetRouter } = await client.contracts(); +// ANCHOR_END: init-client +expect(bridgehub.target).toContain('0x'); + +ethersClient = client; +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the cleint types are updated', async () => { + const _clientType: Exact = true; +}); + +it('ensures the client addresses', async () => { +const client = ethersClient; + +// ANCHOR: ensureAddresses +const a = await client.ensureAddresses(); +/* +{ + bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault, + l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem +} +*/ +// ANCHOR_END: ensureAddresses +}); + +it('gets contracts from the client', async () => { +const client = ethersClient; + +// ANCHOR: contracts +const c = await client.contracts(); +const bh = c.bridgehub; +await bh.getAddress(); +// ANCHOR_END: contracts +}); + +it('refreshes the client', async () => { +const client = ethersClient; + +// ANCHOR: refresh +client.refresh(); +await client.ensureAddresses(); +// ANCHOR_END: refresh +}); + +it('gets the base token from the client', async () => { +const client = ethersClient; + +// ANCHOR: base +const base = await client.baseToken(6565n); +// ANCHOR_END: base +}); + +}); diff --git a/docs/snippets/ethers/reference/contracts.test.ts b/docs/snippets/ethers/reference/contracts.test.ts new file mode 100644 index 0000000..b16b9d2 --- /dev/null +++ b/docs/snippets/ethers/reference/contracts.test.ts @@ -0,0 +1,89 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { JsonRpcProvider, Wallet } from 'ethers'; +import { createEthersClient, createEthersSdk} from '../../../../src/adapters/ethers'; +// ANCHOR_END: imports +import type { EthersSdk } from '../../../../src/adapters/ethers'; + +describe('ethers contracts', () => { + +let ethersSdk: EthersSdk; + +beforeAll(async() => { +// ANCHOR: init-sdk +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// sdk.contracts β†’ ContractsResource +// ANCHOR_END: init-sdk + +ethersSdk = sdk; +}) + +it('gets the native token vault contract', async () => { +const sdk = ethersSdk; + +// ANCHOR: ntv +const addresses = await sdk.contracts.addresses(); +const { l1NativeTokenVault, l2AssetRouter } = await sdk.contracts.instances(); + +const ntv = await sdk.contracts.l1NativeTokenVault(); +// ANCHOR_END: ntv +expect(ntv.target).toContain("0x"); +expect(l1NativeTokenVault.target).toContain("0x"); +expect(l2AssetRouter.target).toContain("0x"); +expect(addresses.bridgehub).toContain("0x"); +}); + +it('gets contract addresses from the sdk', async () => { +const sdk = ethersSdk; + +// ANCHOR: addresses +const a = await sdk.contracts.addresses(); +/* +{ + bridgehub, + l1AssetRouter, + l1Nullifier, + l1NativeTokenVault, + l2AssetRouter, + l2NativeTokenVault, + l2BaseTokenSystem +} +*/ +// ANCHOR_END: addresses +}); + +it('gets contract instances from the sdk', async () => { +const sdk = ethersSdk; + +// ANCHOR: instances +const c = await sdk.contracts.instances(); +/* +{ + bridgehub, + l1AssetRouter, + l1Nullifier, + l1NativeTokenVault, + l2AssetRouter, + l2NativeTokenVault, + l2BaseTokenSystem +} +*/ +// ANCHOR_END: instances + +}); + +it('gets the l2 asset router contract from the sdk', async () => { +const sdk = ethersSdk; + +// ANCHOR: router +const router = await sdk.contracts.l2AssetRouter(); +// ANCHOR_END: router +}); + +}); diff --git a/docs/snippets/ethers/reference/deposits.test.ts b/docs/snippets/ethers/reference/deposits.test.ts new file mode 100644 index 0000000..d926569 --- /dev/null +++ b/docs/snippets/ethers/reference/deposits.test.ts @@ -0,0 +1,322 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +// ANCHOR_END: imports +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core/constants'; +// ANCHOR_END: eth-import +import type { EthersSdk } from '../../../../src/adapters/ethers'; +import type { Address, Hex } from 'viem'; +import type { Exact } from "../../core/types"; +import type { DepositStatus as DStatus, DepositWaitable as DWaitable, DepositParams as DParams } from '../../../../src/core'; + +// ANCHOR: params-type +interface DepositParams { + token: Address; + amount: bigint; + to?: Address; + refundRecipient?: Address; + l2GasLimit?: bigint; + gasPerPubdata?: bigint; + operatorTip?: bigint; + l1TxOverrides?: TxOverrides; +} + +type TxOverrides = { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas?: bigint | undefined; +} +// ANCHOR_END: params-type + +// ANCHOR: quote-type +type DepositRoute = 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; + +type L1DepositFeeParams = { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas?: bigint; + maxTotal: bigint; +}; + +type L2DepositFeeParams = { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas?: bigint; + total: bigint; + baseCost: bigint; + gasPerPubdata: bigint; + operatorTip?: bigint; +}; + +type DepositFeeBreakdown = { + token: `0x${string}`; + maxTotal: bigint; + mintValue?: bigint | undefined; + l1?: L1DepositFeeParams | undefined; + l2?: L2DepositFeeParams; +} + +interface ApprovalNeed { + token: Address; + spender: Address; + amount: bigint; +} + +/** Quote */ +interface DepositQuote { + route: DepositRoute; + approvalsNeeded: readonly ApprovalNeed[]; + amounts: { + transfer: { token: Address; amount: bigint }; + }; + fees: DepositFeeBreakdown; + /** + * @deprecated Use `fees.components?.l2BaseCost` instead. + * Will be removed in a future release. + */ + baseCost?: bigint; + /** + * @deprecated Use `fees.components?.mintValue` instead. + * Will be removed in a future release. + */ + mintValue?: bigint; +} +// ANCHOR_END: quote-type + +// ANCHOR: plan-type +interface PlanStep { + key: string; + kind: string; + description: string; + /** Adapter-specific request (ethers TransactionRequest, viem WriteContractParameters, etc.) */ + tx: Tx; + /** Optional compact, human-friendly view for logging/UI */ + preview?: Preview; +} + +interface Plan { + route: Route; + summary: Quote; + steps: Array>; +} + +/** Plan (Tx generic) */ +type DepositPlan = Plan; +// ANCHOR_END: plan-type + +// ANCHOR: wait-type +interface Handle, Route, PlanT> { + kind: 'deposit' | 'withdrawal'; + route?: Route; + stepHashes: TxHashMap; // step key -> tx hash + plan: PlanT; +} + +/** Handle */ +interface DepositHandle + extends Handle, DepositRoute, DepositPlan> { + kind: 'deposit'; + l1TxHash: Hex; + l2ChainId?: number; + l2TxHash?: Hex; +} + +/** Waitable */ +type DepositWaitable = Hex | { l1TxHash: Hex } | DepositHandle; +// ANCHOR_END: wait-type + +// ANCHOR: status-type +// Status and phases +type DepositPhase = + | 'L1_PENDING' + | 'L1_INCLUDED' // L1 included, L2 hash not derived yet + | 'L2_PENDING' // we have L2 hash, but no receipt yet + | 'L2_EXECUTED' // L2 receipt.status === 1 + | 'L2_FAILED' // L2 receipt.status === 0 + | 'UNKNOWN'; + +// Deposit Status +type DepositStatus = { + phase: DepositPhase; + l1TxHash: Hex; + l2TxHash?: Hex; +}; +// ANCHOR_END: status-type + +describe('ethers deposits', () => { + + let ethersSDK: EthersSdk; + let me: Wallet; + +beforeAll(() => { +// ANCHOR: init-sdk +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// sdk.deposits β†’ DepositsResource +// ANCHOR_END: init-sdk + ethersSDK = sdk; + me = signer +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the deposit types are updated', async () => { + const _paramsType: Exact = true; + const _waitableType: Exact = true; + const _statusType: Exact = true; +}); + +it('creates a deposit', async () => { +const signer = me; +const sdk = ethersSDK; +// ANCHOR: create-deposit +const depositHandle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: await signer.getAddress() as `0x${string}`, +}); + +const l2TxReceipt = await sdk.deposits.wait(depositHandle, { for: 'l2' }); // null only if no L1 hash +// ANCHOR_END: create-deposit +}); + +it('creates a deposit 2', async () => { +const signer = me; +const sdk = ethersSDK; +const to = await signer.getAddress() as `0x${string}`; +const token = ETH_ADDRESS; +const amount = parseEther("0.01"); + +// ANCHOR: handle +const handle = await sdk.deposits.create({ token, amount, to }); +/* +{ + kind: "deposit", + l1TxHash: Hex, + stepHashes: Record, + plan: DepositPlan +} +*/ +// ANCHOR_END: handle + +// ANCHOR: status +const s = await sdk.deposits.status(handle); +// { phase, l1TxHash, l2TxHash? } +// ANCHOR_END: status +expect(s.phase).toBeString(); + +// ANCHOR: wait +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +// ANCHOR_END: wait +expect(l1Receipt?.hash).toContain("0x"); +expect(l2Receipt?.hash).toContain("0x"); +}); + +it('creates a deposit plan', async () => { +const signer = me; +const sdk = ethersSDK; +const to = await signer.getAddress() as `0x${string}`; +const token = ETH_ADDRESS; +const amount = parseEther("0.01"); + +// ANCHOR: plan-deposit +const plan = await sdk.deposits.prepare({ + token, + amount, + to +}); +/* +{ + route, + summary: DepositQuote, + steps: [ + { key: "approve:USDC", kind: "approve", tx: TransactionRequest }, + { key: "bridge", kind: "bridge", tx: TransactionRequest } + ] +} +*/ +// ANCHOR_END: plan-deposit +expect(plan.steps).toBeArray(); +}); + +it('creates a deposit quote', async () => { +const signer = me; +const sdk = ethersSDK; + +const to = await signer.getAddress() as `0x${string}`; + +// ANCHOR: quote-deposit +const q = await sdk.deposits.quote({ + token: ETH_ADDRESS, + amount: parseEther('0.25'), + to, +}); +/* +{ + route: "eth-base" | "eth-nonbase" | "erc20-base" | "erc20-nonbase", + summary: { + route, + approvalsNeeded: [{ token, spender, amount }], + amounts: { + transfer: { token, amount } + }, + fees: { + token, + maxTotal, + mintValue, + l1: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, maxTotal }, + l2: { total, baseCost, operatorTip, gasLimit, maxFeePerGas, maxPriorityFeePerGas, gasPerPubdata } + }, + baseCost, + mintValue + } +} +*/ +// ANCHOR_END: quote-deposit +expect(q.route).toEqual('eth-base'); +}); + +it('creates a deposit 3', async () => { +const signer = me; +const sdk = ethersSDK; +// ANCHOR: create-eth-deposit +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.001'), + to: await signer.getAddress() as `0x${string}`, +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); +// ANCHOR_END: create-eth-deposit + +// ANCHOR: token-address +const token = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // Example: USDC +// ANCHOR_END: token-address +}); + +it('creates a token deposit', async () => { +const signer = me; +const sdk = ethersSDK; +const token = ETH_ADDRESS; + +// ANCHOR: create-token-deposit +const handle = await sdk.deposits.create({ + token, + amount: 1_000_000n, // 1.0 USDC (6 decimals) + to: await signer.getAddress() as `0x${string}`, +}); + +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +// ANCHOR_END: create-token-deposit + +}); + +}); diff --git a/docs/snippets/ethers/reference/finalization-service.test.ts b/docs/snippets/ethers/reference/finalization-service.test.ts new file mode 100644 index 0000000..24b5878 --- /dev/null +++ b/docs/snippets/ethers/reference/finalization-service.test.ts @@ -0,0 +1,153 @@ +import { beforeAll, describe, it } from 'bun:test'; + +// ANCHOR: imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk, createFinalizationServices } from '../../../../src/adapters/ethers'; +// ANCHOR_END: imports +import { ETH_ADDRESS } from '../../../../src/core/constants'; +import type { EthersSdk, FinalizationServices as FServices } from '../../../../src/adapters/ethers'; +import type { Exact } from "../../core/types"; +import { WithdrawalKey } from '../../../../src/core/types/flows/withdrawals'; +import type { FinalizeDepositParams as FParams, FinalizeReadiness as FReady } from '../../../../src/core/types/flows/withdrawals'; +import type { Address, Hex } from 'viem'; +import type { TransactionReceipt } from 'ethers'; + +// ANCHOR: finalization-types +interface FinalizeDepositParams { + chainId: bigint; + l2BatchNumber: bigint; + l2MessageIndex: bigint; + l2Sender: Address; + l2TxNumberInBatch: number; + message: Hex; + merkleProof: Hex[]; +} + +// Finalization readiness states +// Used for `status()` +type FinalizeReadiness = + | { kind: 'READY' } + | { kind: 'FINALIZED' } + | { + kind: 'NOT_READY'; + // temporary, retry later + reason: 'paused' | 'batch-not-executed' | 'root-missing' | 'unknown'; + detail?: string; + } + | { + kind: 'UNFINALIZABLE'; + // permanent, won’t become ready + reason: 'message-invalid' | 'invalid-chain' | 'settlement-layer' | 'unsupported'; + detail?: string; + }; + +interface FinalizationEstimate { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; +} + +interface FinalizationServices { + /** + * Build finalizeDeposit params. + */ + fetchFinalizeDepositParams( + l2TxHash: Hex, + ): Promise<{ params: FinalizeDepositParams; nullifier: Address }>; + + /** + * Read the Nullifier mapping to check finalization status. + */ + isWithdrawalFinalized(key: WithdrawalKey): Promise; + + /** + * Simulate finalizeDeposit on L1 Nullifier to check readiness. + */ + simulateFinalizeReadiness(params: FinalizeDepositParams): Promise; + + /** + * Estimate gas & fees for finalizeDeposit on L1 Nullifier. + */ + estimateFinalization(params: FinalizeDepositParams): Promise; + + /** + * Call finalizeDeposit on L1 Nullifier. + */ + finalizeDeposit( + params: FinalizeDepositParams, + ): Promise<{ hash: string; wait: () => Promise }>; +} +// ANCHOR_END: finalization-types + +describe('ethers finalization service', () => { + + let ethersSDK: EthersSdk; + let me: Wallet; + let service: FinalizationServices; + +beforeAll(() => { +// ANCHOR: init-sdk +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); // optional +const svc = createFinalizationServices(client); +// ANCHOR_END: init-sdk + ethersSDK = sdk; + me = signer + service = svc; +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the finalize withdraw types are updated', async () => { + const _paramsType: Exact = true; + const _finalizeReadinessType: Exact = true; + const _finalizeServicesType: Exact = true; +}); + +it('creates a withdrawal', async () => { +const signer = me; +const sdk = ethersSDK; +const svc = service; +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: await signer.getAddress() as `0x${string}`, // L1 recipient + }); +await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000 }); + +// ANCHOR: finalize-with-svc +// 1) Build finalize params + discover the L1 Nullifier to call +const { params } = await svc.fetchFinalizeDepositParams(handle.l2TxHash); +const key: WithdrawalKey = { + chainIdL2: params.chainId, + l2BatchNumber: params.l2BatchNumber, + l2MessageIndex: params.l2MessageIndex, +}; +// 2) (Optional) check finalization +const already = await svc.isWithdrawalFinalized(key); +if (already) { + console.log('Already finalized on L1'); +} else { + // 3) Dry-run on L1 to confirm readiness (no gas spent) + const readiness = await svc.simulateFinalizeReadiness(params); + + if (readiness.kind === 'READY') { + // 4) Submit finalize tx + const { hash, wait } = await svc.finalizeDeposit(params); + console.log('L1 finalize tx:', hash); + const rcpt = await wait(); + console.log('Finalized in block:', rcpt.blockNumber); + } else { + console.warn('Not ready to finalize:', readiness); + } +} +// ANCHOR_END: finalize-with-svc +}); + + +}); diff --git a/docs/snippets/ethers/reference/sdk.test.ts b/docs/snippets/ethers/reference/sdk.test.ts new file mode 100644 index 0000000..0434c6d --- /dev/null +++ b/docs/snippets/ethers/reference/sdk.test.ts @@ -0,0 +1,122 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: ethers-import +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +// ANCHOR_END: ethers-import +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core'; +// ANCHOR_END: eth-import +// ANCHOR: sdk-import +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +// ANCHOR_END: sdk-import +import type { EthersSdk } from '../../../../src/adapters/ethers'; + +describe('ethers sdk', () => { + +let ethersSDK: EthersSdk; +let me: Wallet; + +beforeAll(() => { +// ANCHOR: init-sdk +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// ANCHOR_END: init-sdk +ethersSDK = sdk; +me = signer; + +// ANCHOR: erc-20-address +const tokenAddress = '0xTokenL1...'; +// ANCHOR_END: erc-20-address +}) + +it('shows basic use of ethers sdk', async () => { + const sdk = ethersSDK; + const signer = me; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: basic-sdk +// Example: deposit 0.05 ETH L1 β†’ L2 and 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() as `0x${string}`, +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); + +// Example: resolve core contracts +const { l1NativeTokenVault } = await sdk.contracts.instances(); + +// Example: map a token L1 β†’ L2 +const token = await sdk.tokens.resolve(tokenAddress); +console.log(token.l2); +// ANCHOR_END: basic-sdk +}); + +it('gets contract addresses from ethers sdk', async () => { + const sdk = ethersSDK; +// ANCHOR: contract-addresses +const a = await sdk.contracts.addresses(); +// ANCHOR_END: contract-addresses +expect(a.bridgehub).toContain("0x"); +}); + +it('gets contract instances from ethers sdk', async () => { + const sdk = ethersSDK; +// ANCHOR: contract-instances +const c = await sdk.contracts.instances(); +// ANCHOR_END: contract-instances +expect(c.bridgehub.target).toContain("0x"); +}); + +it('gets l1 nullifier contract from ethers sdk', async () => { + const sdk = ethersSDK; +// ANCHOR: nullifier +const nullifier = await sdk.contracts.l1Nullifier(); +// ANCHOR_END: nullifier +expect(nullifier.target).toContain("0x"); +}); + +it('it tries to resolve a token address', async () => { + const sdk = ethersSDK; + const tokenAddress = ETH_ADDRESS; + +// ANCHOR: resolve-token +const token = await sdk.tokens.resolve(tokenAddress); +/* +{ + kind: 'eth' | 'base' | 'erc20', + l1: Address, + l2: Address, + assetId: Hex, + originChainId: bigint, + isChainEthBased: boolean, + baseTokenAssetId: Hex, + wethL1: Address, + wethL2: Address, +} +*/ + +// ANCHOR_END: resolve-token +}); +it('maps token addresses', async () => { + const sdk = ethersSDK; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: map-token +const l2Addr = await sdk.tokens.toL2Address(tokenAddress); +const l1Addr = await sdk.tokens.toL1Address(l2Addr); +// ANCHOR_END: map-token +}); +it('gets token asset ids', async () => { + const sdk = ethersSDK; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: token-asset-ids +const assetId = await sdk.tokens.assetIdOfL1(tokenAddress); +const backL2 = await sdk.tokens.l2TokenFromAssetId(assetId); +// ANCHOR_END: token-asset-ids +}); + +}); diff --git a/docs/snippets/ethers/reference/withdrawals.test.ts b/docs/snippets/ethers/reference/withdrawals.test.ts new file mode 100644 index 0000000..813096c --- /dev/null +++ b/docs/snippets/ethers/reference/withdrawals.test.ts @@ -0,0 +1,338 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; +import { createEthersClient, createEthersSdk } from '../../../../src/adapters/ethers'; +// ANCHOR_END: imports +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core/constants'; +// ANCHOR_END: eth-import +import type { EthersSdk } from '../../../../src/adapters/ethers'; +import type { Address, Hex } from 'viem'; +import type { Exact } from "../../core/types"; +import type { WithdrawalStatus as WStatus, WithdrawalWaitable as WWaitable, WithdrawParams as WParams } from '../../../../src/core/types/flows/withdrawals'; +import type { TransactionReceiptZKsyncOS as ZKReceipt } from '../../../../src/adapters/ethers/resources/withdrawals/routes/types'; +import type { TransactionReceipt } from "ethers"; + +// ANCHOR: params-type +type TxOverrides = { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas?: bigint | undefined; +} + +interface WithdrawParams { + token: Address; + amount: bigint; + to?: Address; + refundRecipient?: Address; + l2TxOverrides?: TxOverrides; +} +// ANCHOR_END: params-type + +// ANCHOR: quote-type +/** Routes */ +type WithdrawRoute = 'base' | 'erc20-nonbase'; + +interface ApprovalNeed { + token: Address; + spender: Address; + amount: bigint; +} + +type L2WithdrawalFeeParams = { + gasLimit: bigint; + maxFeePerGas: bigint; + maxPriorityFeePerGas?: bigint; + total: bigint; +}; + +type WithdrawalFeeBreakdown = { + token: Address; // fee token address + maxTotal: bigint; // max amount that can be charged + mintValue?: bigint; + l2?: L2WithdrawalFeeParams; +}; + +/** Quote */ +interface WithdrawQuote { + route: WithdrawRoute; + approvalsNeeded: readonly ApprovalNeed[]; + amounts: { + transfer: { token: Address; amount: bigint }; + }; + fees: WithdrawalFeeBreakdown; +} +// ANCHOR_END: quote-type + +// ANCHOR: plan-type +interface PlanStep { + key: string; + kind: string; + description: string; + /** Adapter-specific request (ethers TransactionRequest, viem WriteContractParameters, etc.) */ + tx: Tx; + /** Optional compact, human-friendly view for logging/UI */ + preview?: Preview; +} + +interface Plan { + route: Route; + summary: Quote; + steps: Array>; +} + +/** Plan (Tx generic) */ +type WithdrawPlan = Plan; +// ANCHOR_END: plan-type + +// ANCHOR: wait-type +interface Handle, Route, PlanT> { + kind: 'deposit' | 'withdrawal'; + route?: Route; + stepHashes: TxHashMap; // step key -> tx hash + plan: PlanT; +} + +/** Handle */ +interface WithdrawHandle + extends Handle, WithdrawRoute, WithdrawPlan> { + kind: 'withdrawal'; + l2TxHash: Hex; + l1TxHash?: Hex; + l2BatchNumber?: number; + l2MessageIndex?: number; + l2TxNumberInBatch?: number; +} + +/** Waitable */ +type WithdrawalWaitable = Hex | { l2TxHash?: Hex; l1TxHash?: Hex } | WithdrawHandle; + +interface L2ToL1Log { + l2ShardId?: number; + isService?: boolean; + txNumberInBlock?: number; + sender?: Address; + key?: Hex; + value?: Hex; +} + +// L2 receipt augmentation returned by wait({ for: 'l2' }) +type TransactionReceiptZKsyncOS = TransactionReceipt & { + l2ToL1Logs?: L2ToL1Log[]; +}; +// ANCHOR_END: wait-type + +// ANCHOR: status-type +type WithdrawalKey = { + chainIdL2: bigint; + l2BatchNumber: bigint; + l2MessageIndex: bigint; +}; + +type WithdrawalPhase = + | 'L2_PENDING' // tx not in an L2 block yet + | 'L2_INCLUDED' // we have the L2 receipt + | 'PENDING' // inclusion known; proof data not yet derivable/available + | 'READY_TO_FINALIZE' // Ready to call finalize on L1 + | 'FINALIZING' // L1 tx sent but not picked up yet + | 'FINALIZED' // L2-L1 tx finalized on L1 + | 'FINALIZE_FAILED' // prior L1 finalize reverted + | 'UNKNOWN'; + +// Withdrawal Status +type WithdrawalStatus = { + phase: WithdrawalPhase; + l2TxHash: Hex; + l1FinalizeTxHash?: Hex; + key?: WithdrawalKey; +}; +// ANCHOR_END: status-type + + + +describe('ethers withdrawals', () => { + + let ethersSDK: EthersSdk; + let me: Wallet; + +beforeAll(() => { +// ANCHOR: init-sdk +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 = createEthersClient({ l1, l2, signer }); +const sdk = createEthersSdk(client); +// sdk.withdrawals β†’ WithdrawalsResource +// ANCHOR_END: init-sdk + ethersSDK = sdk; + me = signer +}) + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the withdraw types are updated', async () => { + const _paramsType: Exact = true; + const _waitableType: Exact = true; + const _statusType: Exact = true; + const _txReceiptWLogsType: Exact = true; +}); + +it('creates a withdrawal', async () => { +const signer = me; +const sdk = ethersSDK; +// ANCHOR: create-withdrawal +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: await signer.getAddress() as `0x${string}`, // 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); +// ANCHOR_END: create-withdrawal +}); + +it('creates a withdrawal 2', async () => { +const signer = me; +const sdk = ethersSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = await signer.getAddress() as `0x${string}`; + +// ANCHOR: plan +const plan = await sdk.withdrawals.prepare({ token, amount, to }); +/* +{ + route, + summary: WithdrawQuote, + steps: [ + { key, kind, tx: TransactionRequest }, + // … + ] +} +*/ +// ANCHOR_END: plan +expect(plan.route).toEqual("base"); + +// ANCHOR: handle +const handle = await sdk.withdrawals.create({ token, amount, to }); +/* +{ + kind: "withdrawal", + l2TxHash: Hex, + stepHashes: Record, + plan: WithdrawPlan +} +*/ +// ANCHOR_END: handle + +// ANCHOR: status +const s = await sdk.withdrawals.status(handle); +// { phase, l2TxHash, key? } +// ANCHOR_END: status +expect(s.phase).toBeString(); + +// ANCHOR: receipt-1 +const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000, timeoutMs: 15 * 60_000 }); +// ANCHOR_END: receipt-1 + +// ANCHOR: finalize +const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +if (status.phase === 'FINALIZED') { + console.log('L1 tx:', receipt?.hash); +} +// ANCHOR_END: finalize + +// ANCHOR: receipt-2 +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', pollMs: 7000 }); +// ANCHOR_END: receipt-2 +expect(l1Rcpt?.hash).toContain("0x"); +const finalStatus = await sdk.withdrawals.status(handle); +expect(finalStatus.phase).toEqual("FINALIZED"); +}); + +it('creates a plan for withdrawal', async () => { +const signer = me; +const sdk = ethersSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = await signer.getAddress() as `0x${string}`; + +// ANCHOR: plan +const plan = await sdk.withdrawals.prepare({ token, amount, to }); +/* +{ + route, + summary: WithdrawQuote, + steps: [ + { key, kind, tx: TransactionRequest }, + // … + ] +} +*/ +// ANCHOR_END: plan +expect(plan.route).toEqual("base"); +}); + +it('creates a quote for withdrawal', async () => { +const signer = me; +const sdk = ethersSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = await signer.getAddress() as `0x${string}`; + +// ANCHOR: quote +const q = await sdk.withdrawals.quote({ token, amount, to }); +/* +{ + route: "base" | "erc20-nonbase", + summary: { + route, + approvalsNeeded: [{ token, spender, amount }], + amounts: { + transfer: { token, amount } + }, + fees: { + token, + maxTotal, + mintValue, + l2: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, total } + } + } +} +*/ +// ANCHOR_END: quote +expect(q.route).toEqual("base"); +}); + +it('creates a withdrawal 3', async () => { +const signer = me; +const sdk = ethersSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = await signer.getAddress() as `0x${string}`; +// ANCHOR: min-happy-path +const handle = await sdk.withdrawals.create({ token, amount, to }); + +// L2 inclusion +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// Option A: wait for readiness, then finalize +await sdk.withdrawals.wait(handle, { for: 'ready' }); +await sdk.withdrawals.finalize(handle.l2TxHash); + +// Option B: finalize immediately (will throw if not ready) +// await sdk.withdrawals.finalize(handle.l2TxHash); +// ANCHOR_END: min-happy-path +}); + +}); diff --git a/docs/snippets/ethers/withdrawals-erc20.ts b/docs/snippets/ethers/withdrawals-erc20.ts deleted file mode 100644 index f78c976..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 '@matterlabs/zksync-js/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.tokens.toL2Address(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 7f9caa3..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 '@matterlabs/zksync-js/ethers'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/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/chains.ts b/docs/snippets/viem/chains.ts new file mode 100644 index 0000000..c6111c2 --- /dev/null +++ b/docs/snippets/viem/chains.ts @@ -0,0 +1,32 @@ +import { defineChain } from "viem"; + +// Chain configuration +export const l1Chain = defineChain({ + id: 31337, + name: "local L1", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [process.env.L1_RPC!], + }, + }, +}); + +export const l2Chain = defineChain({ + id: 6565, + name: "local L2", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [process.env.L2_RPC!], + }, + }, +}); \ No newline at end of file diff --git a/docs/snippets/viem/deposit-erc20.ts b/docs/snippets/viem/deposit-erc20.ts deleted file mode 100644 index 38ab1e0..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 '@matterlabs/zksync-js/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/guides/deposit-eth-guide.test.ts similarity index 62% rename from docs/snippets/viem/deposit-eth.ts rename to docs/snippets/viem/guides/deposit-eth-guide.test.ts index 2f7611a..a321eb8 100644 --- a/docs/snippets/viem/deposit-eth.ts +++ b/docs/snippets/viem/guides/deposit-eth-guide.test.ts @@ -1,15 +1,41 @@ -// examples/deposit-eth.ts -import { createPublicClient, createWalletClient, http, parseEther, WalletClient } from 'viem'; +import { describe, it } from 'bun:test'; + +// ANCHOR: imports +import { createPublicClient, createWalletClient, defineChain, http, parseEther, WalletClient } from 'viem'; import type { Account, Chain, Transport } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; - -import { createViemSdk, createViemClient } from '@matterlabs/zksync-js/viem'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/core'; +import { createViemSdk, createViemClient } from '../../../../src/adapters/viem'; +import { ETH_ADDRESS } from '../../../../src/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 || ''; +const l1Chain = defineChain({ + id: 31337, + name: "Local L1 Chain", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [L1_RPC], + }, + }, +}); +// ANCHOR_END: imports + +describe('viem deposit ETH guide', () => { + +it('deposits some ETH', async () => { + await main(); +}); + +}); + +// ANCHOR: main async function main() { if (!PRIVATE_KEY || PRIVATE_KEY.length !== 66) { throw new Error('Set your PRIVATE_KEY in the .env file'); @@ -21,6 +47,7 @@ async function main() { const l1 = createPublicClient({ transport: http(L1_RPC) }); const l2 = createPublicClient({ transport: http(L2_RPC) }); const l1Wallet: WalletClient = createWalletClient({ + chain: l1Chain, account, transport: http(L1_RPC), }); @@ -50,23 +77,32 @@ async function main() { } as const; // Quote + // ANCHOR: quote const quote = await sdk.deposits.quote(params); + // ANCHOR_END: quote console.log('QUOTE response:', quote); // Prepare (route + steps, no sends) + // ANCHOR: prepare const prepared = await sdk.deposits.prepare(params); + // ANCHOR_END: prepare console.log('PREPARE response:', prepared); // Create (prepare + send) - const created = await sdk.deposits.create(params); - console.log('CREATE response:', created); + // ANCHOR: create + const handle = await sdk.deposits.create(params); + // ANCHOR_END: create + console.log('CREATE response:', handle); // Status (quick check) - const status = await sdk.deposits.status(created); + // ANCHOR: status + const status = await sdk.deposits.status(handle); /* input can be handle or l1TxHash */ + // status.phase: 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' + // ANCHOR_END: status console.log('STATUS response:', status); // Wait (L1 inclusion) - const l1Receipt = await sdk.deposits.wait(created, { for: 'l1' }); + const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); console.log( 'L1 Included at block:', l1Receipt?.blockNumber, @@ -77,11 +113,11 @@ async function main() { ); // Status again - const status2 = await sdk.deposits.status(created); + const status2 = await sdk.deposits.status(handle); console.log('STATUS2 response:', status2); // Wait for L2 execution - const l2Receipt = await sdk.deposits.wait(created, { for: 'l2' }); + const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); console.log( 'L2 Included at block:', l2Receipt?.blockNumber, @@ -91,8 +127,4 @@ async function main() { l2Receipt?.transactionHash, ); } - -main().catch((e) => { - console.error(e); - process.exit(1); -}); +// ANCHOR_END: main \ No newline at end of file diff --git a/docs/snippets/viem/guides/withdrawals-eth-guide.test.ts b/docs/snippets/viem/guides/withdrawals-eth-guide.test.ts new file mode 100644 index 0000000..df52d6c --- /dev/null +++ b/docs/snippets/viem/guides/withdrawals-eth-guide.test.ts @@ -0,0 +1,228 @@ +import { describe, it } from 'bun:test'; + +import type { ViemSdk } from "../../../../src/adapters/viem"; + +// ANCHOR: imports +import { + createPublicClient, + createWalletClient, + defineChain, + http, + parseEther, + type Account, + type Chain, + type Transport, +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +import { createViemSdk, createViemClient } from '../../../../src/adapters/viem'; +import { ETH_ADDRESS } from '../../../../src/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 || ''; + +const l1Chain = defineChain({ + id: 31337, + name: "Local L1 Chain", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [L1_RPC], + }, + }, +}); + +const l2Chain = defineChain({ + id: 6565, + name: "local L2", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + rpcUrls: { + default: { + http: [L2_RPC], + }, + }, +}); +// ANCHOR_END: imports + +describe('viem withdraw ETH guide', () => { + +it('withdraws some ETH', async () => { + // deposit some ETH first + 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({ + chain: l1Chain, + account, + transport: http(L1_RPC), + }); + const l2Wallet = createWalletClient({ + chain: l2Chain, + account, + transport: http(L2_RPC), + }); + + const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); + const sdk = createViemSdk(client); + const params = { + amount: parseEther('0.2'), + to: account.address, + token: ETH_ADDRESS, + } as const; + + const handle = await sdk.deposits.create(params); + await sdk.deposits.wait(handle, { for: 'l2' }); + + await main(); + await altMethods(sdk, account); +}); + +}); + +// ANCHOR: main +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({ + chain: l1Chain, + account, + transport: http(L1_RPC), + }); + const l2Wallet = createWalletClient({ + chain: l2Chain, + 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) + // ANCHOR: quote + const quote = await sdk.withdrawals.quote(params); + // ANCHOR_END: quote + console.log('QUOTE:', quote); + + // Prepare (no sends) + // ANCHOR: prepare + const plan = await sdk.withdrawals.prepare(params); + // ANCHOR_END: prepare + console.log('PREPARE:', plan); + + // Create (send L2 withdraw) + // ANCHOR: create + const handle = await sdk.withdrawals.create(params); + // ANCHOR_END: create + console.log('CREATE:', handle); + + // Quick status + // ANCHOR: status + const status = await sdk.withdrawals.status(handle.l2TxHash); // input can be handle or l2TxHash + // status.phase: 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' + // ANCHOR_END: status + console.log('STATUS (initial):', status); + + // ANCHOR: wait + // Wait for L2 inclusion + const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); + console.log( + 'L2 included: block=', + l2Receipt?.blockNumber, + 'status=', + l2Receipt?.status, + 'hash=', + l2Receipt?.transactionHash, + ); + + // Wait until ready to finalize + await sdk.withdrawals.wait(handle.l2TxHash, { for: 'ready' }); // becomes finalizable + // ANCHOR_END: wait + console.log('STATUS (ready):', await sdk.withdrawals.status(handle.l2TxHash)); + + // Try to finalize on L1 + const fin = await sdk.withdrawals.tryFinalize(handle.l2TxHash); + console.log('TRY FINALIZE:', fin); + + const l1Receipt = await sdk.withdrawals.wait(handle.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).'); + } +} +// ANCHOR_END: main + +async function altMethods(sdk: ViemSdk, account: Account){ + const me = account.address; + + // Withdraw ETH + const params = { + token: ETH_ADDRESS, + amount: parseEther('0.01'), + to: me, + // l2GasLimit: 300_000n, // optional + } as const; + + const handle = await sdk.withdrawals.create(params); + await sdk.withdrawals.wait(handle.l2TxHash, { for: 'ready' }); + + // ANCHOR: wfinalize + const result = await sdk.withdrawals.finalize(handle.l2TxHash); + console.log('Finalization status:', result.status.phase); + // ANCHOR_END: wfinalize + + // ANCHOR: try-catch-create + try { + const handle = await sdk.withdrawals.create(params); +} catch (e) { + // normalized error envelope (type, operation, message, context, optional revert) +} +// ANCHOR_END: try-catch-create + +// ANCHOR: tryCreate +const r = await sdk.withdrawals.tryCreate(params); + +if (!r.ok) { + console.error('Withdrawal failed:', r.error); +} else { + const handle = r.value; + await sdk.withdrawals.wait(handle.l2TxHash, { for: 'ready' }); + 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); + } +} +// ANCHOR_END: tryCreate + +} \ No newline at end of file diff --git a/docs/snippets/viem/overview/adapter-basic.test.ts b/docs/snippets/viem/overview/adapter-basic.test.ts new file mode 100644 index 0000000..dbfa776 --- /dev/null +++ b/docs/snippets/viem/overview/adapter-basic.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from 'bun:test'; + +// ANCHOR: viem-basic-imports +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +// ANCHOR_END: viem-basic-imports + +import { l1Chain, l2Chain } from '../chains'; + +describe('viem adapter setup', () => { + +it('sets up a basic viem adapter', async () => { +// ANCHOR: init-viem-adapter +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); +// ANCHOR_END: init-viem-adapter +}); + + +}); diff --git a/docs/snippets/viem/overview/adapter.test.ts b/docs/snippets/viem/overview/adapter.test.ts new file mode 100644 index 0000000..dd457e1 --- /dev/null +++ b/docs/snippets/viem/overview/adapter.test.ts @@ -0,0 +1,264 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: viem-adapter-imports +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +import { ETH_ADDRESS } from '../../../../src/core'; +// ANCHOR_END: viem-adapter-imports + +import type { ViemSdk, ViemClient } from '../../../../src/adapters/viem'; +import type { Account } from 'viem'; +import { l1Chain, l2Chain } from '../chains'; + +describe('viem adapter setup', () => { + +let viemSDK: ViemSdk; +let viemClient: ViemClient; +let richAccount: Account; + +beforeAll(async () => { +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); + +// ANCHOR: deposit-quote +const quote = await sdk.deposits.quote({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: account.address, +}); + +console.log('Max total fee:', quote.fees.maxTotal.toString()); +// ANCHOR_END: deposit-quote + +// ANCHOR: viem-deposit +const params = { + amount: parseEther('0.1'), + 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 +// ANCHOR_END: viem-deposit + +viemSDK = sdk; +richAccount = account; +viemClient = client; +}); + +it('deposits some ETH', async () => { +const sdk = viemSDK; +const account = richAccount; + +const params = { + amount: parseEther('0.01'), + to: account.address, + token: ETH_ADDRESS, +} as const; + +const handle = await sdk.deposits.create(params); + +// ANCHOR: deposit-wait +const l1Rcpt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Rcpt = await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 +// ANCHOR_END: deposit-wait + +const handleOrL1Hash = handle; + +// ANCHOR: deposit-status +const s = await sdk.deposits.status(handleOrL1Hash); +// s.phase ∈ 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' +// ANCHOR_END: deposit-status +expect(s.phase).toEqual("L2_EXECUTED"); +}); + +it('try depositing some ETH', async () => { +const sdk = viemSDK; +const account = richAccount; + +const params = { + amount: parseEther('0.0001'), + to: account.address, + token: ETH_ADDRESS, +} as const; + +// ANCHOR: try-deposit +try { + const handle = await sdk.deposits.create(params); +} catch (e) { + // normalized error envelope (type, operation, message, context, revert?) +} +// ANCHOR_END: try-deposit + +// ANCHOR: try-create +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); +} +// ANCHOR_END: try-create +expect(r.ok).toEqual(true); +}); + +it('withdraws some ETH', async () => { +const sdk = viemSDK; +const account = richAccount; + +const params = { + amount: parseEther('0.01'), + to: account.address, + token: ETH_ADDRESS, +} as const; + +const handle = await sdk.withdrawals.create(params); + +// ANCHOR: withdraw-wait +// 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' }); + +// finalize on the L1 +await sdk.withdrawals.tryFinalize(handle.l2TxHash); + +// Wait for L1 finalization β†’ L1 receipt (or null if not retrievable) +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', timeoutMs: 15 * 60_000 }); +// ANCHOR_END: withdraw-wait +expect(l1Rcpt?.status).toEqual("success"); +}); + +it('withdraws some ETH 2', async () => { +const sdk = viemSDK; +const account = richAccount; + +const params = { + amount: parseEther('0.01'), + to: account.address, + token: ETH_ADDRESS, +} as const; + +const handle = await sdk.withdrawals.create(params); + +// ANCHOR: withdraw-poll +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 is finalizable β€” decide whether to retry or show a hint +} +// ANCHOR_END: withdraw-poll +await sdk.withdrawals.tryFinalize(handle.l2TxHash); + +// ANCHOR: withdraw-try-wait +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); +} +// ANCHOR_END: withdraw-try-wait + +const handleOrHash = handle; + +// ANCHOR: withdraw-status +const s = await sdk.withdrawals.status(handleOrHash); +// s.phase ∈ 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +// ANCHOR_END: withdraw-status +expect(s.phase).toEqual("FINALIZED"); +}); + +it('withdraws some ETH 3', async () => { +const sdk = viemSDK; +const account = richAccount; + +// ANCHOR: withdraw-short +// 1) Create on L2 +const withdrawal = await sdk.withdrawals.create({ + token: ETH_ADDRESS, + amount: parseEther('0.01'), + to: account.address, +}); + +// 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 +// ANCHOR_END: withdraw-short +expect(status.phase).toEqual("FINALIZED"); + +}); + +it('withdraws some ETH 3', async () => { +const sdk = viemSDK; +const account = richAccount; + +const withdrawal = await sdk.withdrawals.create({ + token: ETH_ADDRESS, + amount: parseEther('0.01'), + to: account.address, +}); + +const l2TxHash = withdrawal.l2TxHash; + +// ANCHOR: withdraw-by-hash +// 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); +// ANCHOR_END: withdraw-by-hash + +// ANCHOR: withdraw-try-finalize +const r = await sdk.withdrawals.tryFinalize(l2TxHash); +if (!r.ok) { + console.error('Finalize failed:', r.error); +} else { + console.log('Status:', r.value.status); + console.log('Finalized on L1 tx hash?:', r.value.receipt?.transactionHash); +} +// ANCHOR_END: withdraw-try-finalize + + const client = viemClient; + +// ANCHOR: receipt-with-logs +const rcpt = await client.zks.getReceiptWithL2ToL1(l2TxHash); +console.log("L2 to L1 logs:", rcpt?.l2ToL1Logs); // always an array +// ANCHOR_END: receipt-with-logs + +// ANCHOR: log-proof +const proof = await client.zks.getL2ToL1LogProof(l2TxHash, 0); +/* +{ + id: bigint, + batchNumber: bigint, + proof: Hex[] +} +*/ +// ANCHOR_END: log-proof +expect(proof.proof).toBeArray(); +}); + +}); diff --git a/docs/snippets/viem/quickstart/quickstart.test.ts b/docs/snippets/viem/quickstart/quickstart.test.ts new file mode 100644 index 0000000..740eaed --- /dev/null +++ b/docs/snippets/viem/quickstart/quickstart.test.ts @@ -0,0 +1,106 @@ +import { describe, it } from 'bun:test'; + +// ANCHOR: quickstart-imports +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +import { ETH_ADDRESS } from '../../../../src/core'; +// either import the chain from viem/chains or use defineChain +import { l1Chain } from '../chains'; + +const PRIVATE_KEY = process.env.PRIVATE_KEY; +const L1_RPC_URL = process.env.L1_RPC; +const L2_RPC_URL = process.env.L2_RPC; +// ANCHOR_END: quickstart-imports + +describe('viem quickstart', () => { + +it('deposits some ETH and checks balances', async () => { + await main(); +}); + +}); + +// ANCHOR: quickstart-main +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({ chain: l1Chain, account, transport: http(L1_RPC_URL) }); + + // 2. INITIALIZE THE SDK CLIENT & SDK + // 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, + // Optional gas control: + // l1TxOverrides: { + // gasLimit: 280_000n, + // maxFeePerGas: parseEther('0.00000002'), + // maxPriorityFeePerGas: parseEther('0.000000002'), + // }, + }; + + // 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); + */ +} +// ANCHOR_END: quickstart-main \ No newline at end of file diff --git a/docs/snippets/viem/reference/client.test.ts b/docs/snippets/viem/reference/client.test.ts new file mode 100644 index 0000000..a569ed4 --- /dev/null +++ b/docs/snippets/viem/reference/client.test.ts @@ -0,0 +1,106 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: viem-import +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +// ANCHOR_END: viem-import +// ANCHOR: client-import +import { createViemClient } from '../../../../src/adapters/viem'; +// ANCHOR_END: client-import +import type { Address } from 'viem'; +import type { ViemClient, ResolvedAddresses as RAddrs } from '../../../../src/adapters/viem/client'; +import type { Exact } from "../../core/types"; +import { l1Chain, l2Chain } from '../chains'; + +// ANCHOR: resolved-type +type ResolvedAddresses = { + bridgehub: Address; + l1AssetRouter: Address; + l1Nullifier: Address; + l1NativeTokenVault: Address; + l2AssetRouter: Address; + l2NativeTokenVault: Address; + l2BaseTokenSystem: Address; +}; +// ANCHOR_END: resolved-type + +describe('viem client', () => { + +let viemClient: ViemClient; + +beforeAll(async () => { +// ANCHOR: init-client +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_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(); +// ANCHOR_END: init-client +expect(bridgehub.address).toContain('0x'); + +viemClient = client; +}); + +// this test will always succeed +// but any errors will be highlighted +it('checks to see if the cleint types are updated', async () => { + const _clientType: Exact = true; +}); + +it('ensures the client addresses', async () => { +const client = viemClient; + +// ANCHOR: ensureAddresses +const a = await client.ensureAddresses(); +/* +{ + bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault, + l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem +} +*/ +// ANCHOR_END: ensureAddresses +}); + +it('gets contracts from the client', async () => { +const client = viemClient; + +// ANCHOR: contracts +const c = await client.contracts(); +const bh = c.bridgehub; +const bhAddress = bh.address; // bh.read.*, bh.write.*, bh.simulate.* +// ANCHOR_END: contracts +}); + +it('refreshes the client', async () => { +const client = viemClient; + +// ANCHOR: refresh +client.refresh(); +await client.ensureAddresses(); +// ANCHOR_END: refresh +}); + +it('gets the base token from the client', async () => { +const client = viemClient; +// ANCHOR: base +const base = await client.baseToken(6565n); +// ANCHOR_END: base +}); + +it('gets the l2 wallet from the client', async () => { +const client = viemClient; +// ANCHOR: l2-wallet +const w = client.getL2Wallet(); // ensures L2 writes are possible +// ANCHOR_END: l2-wallet +}); + +}); diff --git a/docs/snippets/viem/reference/contracts.test.ts b/docs/snippets/viem/reference/contracts.test.ts new file mode 100644 index 0000000..bd739c1 --- /dev/null +++ b/docs/snippets/viem/reference/contracts.test.ts @@ -0,0 +1,94 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createPublicClient, createWalletClient, http } from 'viem'; +// ANCHOR_END: imports +import { l1Chain, l2Chain } from '../chains'; +import type { ViemSdk } from '../../../../src/adapters/viem'; + +describe('viem contracts', () => { + +let viemSdk: ViemSdk; + +beforeAll(async() => { +// ANCHOR: init-sdk +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); +// sdk.contracts β†’ ContractsResource +// ANCHOR_END: init-sdk + +viemSdk = sdk; +}) + +it('gets the native token vault contract', async () => { +const sdk = viemSdk; + +// ANCHOR: ntv +const addresses = await sdk.contracts.addresses(); +const { l1NativeTokenVault, l2AssetRouter } = await sdk.contracts.instances(); + +const ntv = await sdk.contracts.l1NativeTokenVault(); +// ANCHOR_END: ntv +expect(ntv.address).toContain("0x"); +expect(l1NativeTokenVault.address).toContain("0x"); +expect(l2AssetRouter.address).toContain("0x"); +expect(addresses.bridgehub).toContain("0x"); +}); + +it('gets contract addresses from the sdk', async () => { +const sdk = viemSdk; + +// ANCHOR: addresses +const a = await sdk.contracts.addresses(); +/* +{ + bridgehub, + l1AssetRouter, + l1Nullifier, + l1NativeTokenVault, + l2AssetRouter, + l2NativeTokenVault, + l2BaseTokenSystem +} +*/ +// ANCHOR_END: addresses +}); + +it('gets contract instances from the sdk', async () => { +const sdk = viemSdk; + +// ANCHOR: instances +const c = await sdk.contracts.instances(); +/* +{ + bridgehub, + l1AssetRouter, + l1Nullifier, + l1NativeTokenVault, + l2AssetRouter, + l2NativeTokenVault, + l2BaseTokenSystem +} +*/ +// ANCHOR_END: instances + +}); + +it('gets the l2 asset router contract from the sdk', async () => { +const sdk = viemSdk; + +// ANCHOR: router +const router = await sdk.contracts.l2AssetRouter(); +// ANCHOR_END: router +}); + +}); diff --git a/docs/snippets/viem/reference/deposits.test.ts b/docs/snippets/viem/reference/deposits.test.ts new file mode 100644 index 0000000..b61f026 --- /dev/null +++ b/docs/snippets/viem/reference/deposits.test.ts @@ -0,0 +1,175 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +// ANCHOR_END: imports +import { l1Chain, l2Chain } from '../chains'; +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core/constants'; +// ANCHOR_END: eth-import +import type { ViemSdk } from '../../../../src/adapters/viem'; +import type { Account } from 'viem'; + +describe('viem deposits', () => { + + let viemSDK: ViemSdk; + let me: Account; + +beforeAll(() => { +// ANCHOR: init-sdk +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); +// sdk.deposits β†’ DepositsResource +// ANCHOR_END: init-sdk + viemSDK = sdk; + me = account; +}) + +it('creates a deposit', async () => { +const account = me; +const sdk = viemSDK; +// ANCHOR: create-deposit +const depositHandle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.1'), + to: account.address, +}); + +const l2TxReceipt = await sdk.deposits.wait(depositHandle, { for: 'l2' }); // null only if no L1 hash +// ANCHOR_END: create-deposit +}); + +it('creates a deposit 2', async () => { +const account = me; +const sdk = viemSDK; +const to = account.address; +const token = ETH_ADDRESS; +const amount = parseEther("0.01"); + +// ANCHOR: handle +const handle = await sdk.deposits.create({ token, amount, to }); +/* +{ + kind: "deposit", + l1TxHash: Hex, + stepHashes: Record, + plan: DepositPlan +} +*/ +// ANCHOR_END: handle + + +// ANCHOR: status +const s = await sdk.deposits.status(handle); +// { phase, l1TxHash, l2TxHash? } +// ANCHOR_END: status +expect(s.phase).toBeString(); + +// ANCHOR: wait +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +// ANCHOR_END: wait +expect(l1Receipt?.transactionHash).toContain("0x"); +expect(l2Receipt?.transactionHash).toContain("0x"); +}); + +it('creates a deposit quote', async () => { +const account = me; +const sdk = viemSDK; +const to = account.address; +const token = ETH_ADDRESS; +const amount = parseEther("0.01"); + +// ANCHOR: plan-deposit +const plan = await sdk.deposits.prepare({ + token, + amount, + to +}); +/* +{ + route, + summary: DepositQuote, + steps: [ + { key: "approve:USDC", kind: "approve", tx: TransactionRequest }, + { key: "bridge", kind: "bridge", tx: TransactionRequest } + ] +} +*/ +// ANCHOR_END: plan-deposit +expect(plan.steps).toBeArray(); +}); + +it('creates a deposit plan', async () => { +const account = me; +const sdk = viemSDK; +const to = account.address; +const token = ETH_ADDRESS; +const amount = parseEther("0.01"); + +// ANCHOR: plan-deposit +const plan = await sdk.deposits.prepare({ + token, + amount, + to +}); +/* +{ + route, + summary: DepositQuote, + steps: [ + { key: "approve:USDC", kind: "approve", tx: TransactionRequest }, + { key: "bridge", kind: "bridge", tx: TransactionRequest } + ] +} +*/ +// ANCHOR_END: plan-deposit +expect(plan.steps).toBeArray(); +}); + +it('creates a deposit 3', async () => { +const account = me; +const sdk = viemSDK; +// ANCHOR: create-eth-deposit +const handle = await sdk.deposits.create({ + token: ETH_ADDRESS, + amount: parseEther('0.001'), + to: account.address, +}); + +await sdk.deposits.wait(handle, { for: 'l2' }); +// ANCHOR_END: create-eth-deposit +expect(handle.plan.route).toEqual('eth-base'); + +// ANCHOR: token-address +const token = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // Example: USDC +// ANCHOR_END: token-address +}); + +it('creates a token deposit', async () => { +const account = me; +const sdk = viemSDK; +const token = ETH_ADDRESS; + +// ANCHOR: create-token-deposit +const handle = await sdk.deposits.create({ + token, + amount: 1_000_000n, // 1.0 USDC (6 decimals) + to: account.address +}); + +const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +// ANCHOR_END: create-token-deposit +expect(l1Receipt?.transactionHash).toContain("0x"); +}); + +}); diff --git a/docs/snippets/viem/reference/finalization-service.test.ts b/docs/snippets/viem/reference/finalization-service.test.ts new file mode 100644 index 0000000..623a526 --- /dev/null +++ b/docs/snippets/viem/reference/finalization-service.test.ts @@ -0,0 +1,80 @@ +import { beforeAll, describe, it } from 'bun:test'; + +// ANCHOR: imports +import { privateKeyToAccount } from 'viem/accounts'; +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +import { createViemClient, createViemSdk, createFinalizationServices } from '../../../../src/adapters/viem'; +// ANCHOR_END: imports +import { ETH_ADDRESS } from '../../../../src/core/constants'; +import { l1Chain, l2Chain } from '../chains'; +import type { FinalizationServices, ViemSdk } from '../../../../src/adapters/viem'; +import type { Account } from 'viem'; +import type { WithdrawalKey } from '../../../../src/core/types/flows/withdrawals'; + +describe('viem finalization service', () => { + + let viemSDK: ViemSdk; + let me: Account; + let service: FinalizationServices; + +beforeAll(() => { +// ANCHOR: init-sdk +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); // optional +const svc = createFinalizationServices(client); +// ANCHOR_END: init-sdk + viemSDK = sdk; + me = account; + service = svc; +}) + +it('creates a withdrawal', async () => { +const account = me; +const sdk = viemSDK; +const svc = service; +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: account.address, // L1 recipient + }); +await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000 }); + +// ANCHOR: finalize-with-svc +// 1) Build finalize params + discover the L1 Nullifier to call +const { params } = await svc.fetchFinalizeDepositParams(handle.l2TxHash); +const key: WithdrawalKey = { + chainIdL2: params.chainId, + l2BatchNumber: params.l2BatchNumber, + l2MessageIndex: params.l2MessageIndex, +}; +// 2) (Optional) check finalization +const already = await svc.isWithdrawalFinalized(key); +if (already) { + console.log('Already finalized on L1'); +} else { + // 3) Dry-run on L1 to confirm readiness (no gas spent) + const readiness = await svc.simulateFinalizeReadiness(params); + + if (readiness.kind === 'READY') { + // 4) Submit finalize tx + const { hash, wait } = await svc.finalizeDeposit(params); + console.log('L1 finalize tx:', hash); + const rcpt = await wait(); + console.log('Finalized in block:', rcpt.blockNumber); + } else { + console.warn('Not ready to finalize:', readiness); + } +} +// ANCHOR_END: finalize-with-svc +}); + + +}); diff --git a/docs/snippets/viem/reference/sdk.test.ts b/docs/snippets/viem/reference/sdk.test.ts new file mode 100644 index 0000000..f2defbb --- /dev/null +++ b/docs/snippets/viem/reference/sdk.test.ts @@ -0,0 +1,128 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: viem-import +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +// ANCHOR_END: viem-import +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core'; +// ANCHOR_END: eth-import +// ANCHOR: sdk-import +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +// ANCHOR_END: sdk-import +import type { ViemSdk } from '../../../../src/adapters/viem'; +import type { Account } from 'viem'; +import { l1Chain, l2Chain } from '../chains'; + +describe('viem sdk', () => { + +let viemSDK: ViemSdk; +let me: Account; + +beforeAll(() => { +// ANCHOR: init-sdk +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); +// ANCHOR_END: init-sdk +viemSDK = sdk; +me = account; + +// ANCHOR: erc-20-address +const tokenAddress = '0xYourToken'; +// ANCHOR_END: erc-20-address +}) + +it('shows basic use of viem sdk', async () => { + const sdk = viemSDK; + const account = me; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: basic-sdk +// 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 + amount: 50_000_000_000_000_000n, // 0.05 ETH in wei + to: 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.contracts.instances(); +const token = await sdk.tokens.resolve(tokenAddress); +console.log(token.l2); +// ANCHOR_END: basic-sdk +}); + +it('gets contract addresses from viem sdk', async () => { + const sdk = viemSDK; +// ANCHOR: contract-addresses +const a = await sdk.contracts.addresses(); +// ANCHOR_END: contract-addresses +expect(a.bridgehub).toContain("0x"); +}); + +it('gets contract instances from viem sdk', async () => { + const sdk = viemSDK; +// ANCHOR: contract-instances +const c = await sdk.contracts.instances(); +const bridgehub = c.bridgehub; +// ANCHOR_END: contract-instances +expect(bridgehub.address).toContain("0x"); +}); + +it('gets l1 nullifier contract from viem sdk', async () => { + const sdk = viemSDK; +// ANCHOR: nullifier +const nullifier = await sdk.contracts.l1Nullifier(); +// ANCHOR_END: nullifier +expect(nullifier.address).toContain("0x"); +}); + +it('it tries to resolve a token address', async () => { + const sdk = viemSDK; + const tokenAddress = ETH_ADDRESS; + +// ANCHOR: resolve-token +const token = await sdk.tokens.resolve(tokenAddress); +/* +{ + kind: 'eth' | 'base' | 'erc20', + l1: Address, + l2: Address, + assetId: Hex, + originChainId: bigint, + isChainEthBased: boolean, + baseTokenAssetId: Hex, + wethL1: Address, + wethL2: Address, +} +*/ + +// ANCHOR_END: resolve-token +}); + +it('maps token addresses', async () => { + const sdk = viemSDK; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: map-token +const l2Addr = await sdk.tokens.toL2Address(tokenAddress); +const l1Addr = await sdk.tokens.toL1Address(l2Addr); +// ANCHOR_END: map-token +}); + +it('gets token asset ids', async () => { + const sdk = viemSDK; + const tokenAddress = ETH_ADDRESS; +// ANCHOR: token-asset-ids +const assetId = await sdk.tokens.assetIdOfL1(tokenAddress); +const backL2 = await sdk.tokens.l2TokenFromAssetId(assetId); +// ANCHOR_END: token-asset-ids +}); + +}); diff --git a/docs/snippets/viem/reference/withdrawals.test.ts b/docs/snippets/viem/reference/withdrawals.test.ts new file mode 100644 index 0000000..fcf13cf --- /dev/null +++ b/docs/snippets/viem/reference/withdrawals.test.ts @@ -0,0 +1,178 @@ +import { beforeAll, describe, expect, it } from 'bun:test'; + +// ANCHOR: imports +import { createViemClient, createViemSdk } from '../../../../src/adapters/viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; +// ANCHOR_END: imports +// ANCHOR: eth-import +import { ETH_ADDRESS } from '../../../../src/core/constants'; +// ANCHOR_END: eth-import +import type { ViemSdk } from '../../../../src/adapters/viem'; +import { l1Chain, l2Chain } from '../chains'; +import type { Account } from 'viem'; + +describe('viem withdrawals', () => { + + let viemSDK: ViemSdk; + let me: Account; + +beforeAll(() => { +// ANCHOR: init-sdk +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({ chain: l1Chain, account, transport: http(process.env.L1_RPC!) }); +const l2Wallet = createWalletClient({ chain: l2Chain, account, transport: http(process.env.L2_RPC!) }); + +const client = createViemClient({ l1, l2, l1Wallet, l2Wallet }); +const sdk = createViemSdk(client); +// sdk.withdrawals β†’ WithdrawalsResource +// ANCHOR_END: init-sdk + viemSDK = sdk; + me = account; +}) + +it('creates a withdrawal', async () => { +const account = me; +const sdk = viemSDK; +// ANCHOR: create-withdrawal +const handle = await sdk.withdrawals.create({ + token: ETH_ADDRESS, // ETH sentinel supported + amount: parseEther('0.1'), + to: account.address, // 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); +// ANCHOR_END: create-withdrawal +}); + +it('creates a withdrawal 2', async () => { +const account = me; +const sdk = viemSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = account.address; + +// ANCHOR: handle +const handle = await sdk.withdrawals.create({ token, amount, to }); +/* +{ + kind: "withdrawal", + l2TxHash: Hex, + stepHashes: Record, + plan: WithdrawPlan +} +*/ +// ANCHOR_END: handle + +// ANCHOR: status +const s = await sdk.withdrawals.status(handle); +// { phase, l2TxHash, key? } +// ANCHOR_END: status +expect(s.phase).toBeString(); + +// ANCHOR: receipt-1 +const l2Rcpt = await sdk.withdrawals.wait(handle, { for: 'l2' }); +await sdk.withdrawals.wait(handle, { for: 'ready', pollMs: 6000, timeoutMs: 15 * 60_000 }); +// ANCHOR_END: receipt-1 + +// ANCHOR: finalize +const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +if (status.phase === 'FINALIZED') { + console.log('L1 tx:', receipt?.transactionHash); +} +// ANCHOR_END: finalize + +// ANCHOR: receipt-2 +const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', pollMs: 7000 }); +// ANCHOR_END: receipt-2 +expect(l1Rcpt?.transactionHash).toContain("0x"); +const finalStatus = await sdk.withdrawals.status(handle); +expect(finalStatus.phase).toEqual("FINALIZED"); +}); + +it('creates a plan for a withdrawal', async () => { +const account = me; +const sdk = viemSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = account.address; + +// ANCHOR: plan +const plan = await sdk.withdrawals.prepare({ token, amount, to }); +/* +{ + route, + summary: WithdrawQuote, + steps: [ + { key, kind, tx: TransactionRequest }, + // … + ] +} +*/ +// ANCHOR_END: plan +expect(plan.route).toEqual("base"); +}); + +it('gets a quote for a withdrawal', async () => { +const account = me; +const sdk = viemSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = account.address; + +// ANCHOR: quote +const q = await sdk.withdrawals.quote({ token, amount, to }); +/* +{ + route: "base" | "erc20-nonbase", + summary: { + route, + approvalsNeeded: [{ token, spender, amount }], + amounts: { + transfer: { token, amount } + }, + fees: { + token, + maxTotal, + mintValue, + l2: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, total } + } + } +} +*/ +// ANCHOR_END: quote +expect(q.route).toEqual("base"); +}); + +it('creates a withdrawal 3', async () => { +const account = me; +const sdk = viemSDK; +const token = ETH_ADDRESS; +const amount = parseEther('0.01'); +const to = account.address; +// ANCHOR: min-happy-path +const handle = await sdk.withdrawals.create({ token, amount, to }); + +// L2 inclusion +await sdk.withdrawals.wait(handle, { for: 'l2' }); + +// Option A: wait for readiness, then finalize +await sdk.withdrawals.wait(handle, { for: 'ready' }); +await sdk.withdrawals.finalize(handle.l2TxHash); + +// Option B: finalize immediately (will throw if not ready) +// await sdk.withdrawals.finalize(handle.l2TxHash); +// ANCHOR_END: min-happy-path +}); + +}); diff --git a/docs/snippets/viem/withdrawals-erc20.ts b/docs/snippets/viem/withdrawals-erc20.ts deleted file mode 100644 index 5ccce16..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 '@matterlabs/zksync-js/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.tokens.toL2Address(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 ee0139c..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 '@matterlabs/zksync-js/viem'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/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/guides/deposits/ethers.md b/docs/src/guides/deposits/ethers.md index f637268..a40b3ce 100644 --- a/docs/src/guides/deposits/ethers.md +++ b/docs/src/guides/deposits/ethers.md @@ -30,7 +30,14 @@ A fast path to deposit **ETH / ERC-20** from L1 β†’ ZKsync (L2) using the **ethe ## Fast path (one-shot) ```ts -{{#include ../../../snippets/ethers/deposit-eth.ts}} +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:imports}} + +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:main}} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); ``` - `create()` prepares **and** sends. @@ -43,21 +50,21 @@ A fast path to deposit **ETH / ERC-20** from L1 β†’ ZKsync (L2) using the **ethe Preview fees/steps and whether an approve is required. ```ts -const quote = await sdk.deposits.quote(params); +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:quote}} ``` **2. Prepare (build txs, don’t send)** Get `TransactionRequest[]` for signing/UX. ```ts -const plan = await sdk.deposits.prepare(params); +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:prepare}} ``` **3. Create (send)** Use defaults, or send your prepared txs if you customized. ```ts -const handle = await sdk.deposits.create(params); +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:create}} ``` ## Track progress (status vs wait) @@ -65,15 +72,13 @@ const handle = await sdk.deposits.create(params); **Non-blocking snapshot** ```ts -const s = await sdk.deposits.status(handle /* or l1TxHash */); -// 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' +{{#include ../../../snippets/ethers/guides/deposit-eth-guide.test.ts:status}} ``` **Block until checkpoint** ```ts -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +{{#include ../../../snippets/viem/overview/adapter.test.ts:deposit-wait}} ``` --- @@ -83,11 +88,7 @@ const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); **Exceptions** ```ts -try { - const handle = await sdk.deposits.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, revert?) -} +{{#include ../../../snippets/viem/overview/adapter.test.ts:try-deposit}} ``` **No-throw style** @@ -101,16 +102,7 @@ These never throwβ€”so you don’t need a `try/catch`. Instead they return: 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); -} +{{#include ../../../snippets/viem/overview/adapter.test.ts:try-create}} ``` ## Troubleshooting diff --git a/docs/src/guides/deposits/viem.md b/docs/src/guides/deposits/viem.md index 58b1bb6..303c1ae 100644 --- a/docs/src/guides/deposits/viem.md +++ b/docs/src/guides/deposits/viem.md @@ -28,7 +28,14 @@ A fast path to deposit **ETH / ERC-20** from L1 β†’ ZKsync (L2) using the **viem ## Fast path (one-shot) ```ts -{{#include ../../../snippets/viem/deposit-eth.ts}} +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:imports}} + +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:main}} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); ``` - `create()` prepares **and** sends. @@ -42,21 +49,21 @@ A fast path to deposit **ETH / ERC-20** from L1 β†’ ZKsync (L2) using the **viem Preview fees/steps and whether an approve is required. ```ts -const quote = await sdk.deposits.quote(params); +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:quote}} ``` **2. Prepare (build txs, don’t send)** Get `TransactionRequest[]` for signing/UX. ```ts -const plan = await sdk.deposits.prepare(params); +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:prepare}} ``` **3. Create (send)** Use defaults, or send your prepared txs if you customized. ```ts -const handle = await sdk.deposits.create(params); +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:create}} ``` ## Track progress (status vs wait) @@ -64,15 +71,13 @@ const handle = await sdk.deposits.create(params); **Non-blocking snapshot** ```ts -const s = await sdk.deposits.status(handle /* or l1TxHash */); -// 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' +{{#include ../../../snippets/viem/guides/deposit-eth-guide.test.ts:status}} ``` **Block until checkpoint** ```ts -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); +{{#include ../../../snippets/viem/overview/adapter.test.ts:deposit-wait}} ``` ## Error handling patterns @@ -80,11 +85,7 @@ const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); **Exceptions** ```ts -try { - const handle = await sdk.deposits.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, revert?) -} +{{#include ../../../snippets/viem/overview/adapter.test.ts:try-deposit}} ``` **No-throw style** @@ -98,16 +99,7 @@ These never throwβ€”so you don’t need a `try/catch`. Instead they return: 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); -} +{{#include ../../../snippets/viem/overview/adapter.test.ts:try-create}} ``` ## Troubleshooting diff --git a/docs/src/guides/withdrawals/ethers.md b/docs/src/guides/withdrawals/ethers.md index c28b784..91ba66d 100644 --- a/docs/src/guides/withdrawals/ethers.md +++ b/docs/src/guides/withdrawals/ethers.md @@ -26,11 +26,17 @@ Withdrawals are a **two-step process**: | `refundRecipient` | No | L2 address to receive fee refunds (if applicable) | | `l2TxOverrides` | No | L2 tx overrides (e.g. gasLimit, maxFeePerGas, maxPriorityFeePerGas) | - ## Fast path (one-shot) ```ts -{{#include ../../../snippets/ethers/withdrawals-eth.ts}} +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:imports}} + +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:main}} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); ``` - `create()` prepares **and** sends the L2 withdrawal. @@ -43,19 +49,19 @@ Withdrawals are a **two-step process**: **1. Quote (no side-effects)** ```ts -const quote = await sdk.withdrawals.quote(params); +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:quote}} ``` **2. Prepare (build txs, don’t send)** ```ts -const plan = await sdk.withdrawals.prepare(params); +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:prepare}} ``` **3. Create (send)** ```ts -const handle = await sdk.withdrawals.create(params); +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:create}} ``` ## Track progress (status vs wait) @@ -63,22 +69,20 @@ const handle = await sdk.withdrawals.create(params); **Non-blocking snapshot** ```ts -const s = await sdk.withdrawals.status(handle /* or l2TxHash */); -// 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:status}} ``` **Block until checkpoint** ```ts -const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); -await sdk.withdrawals.wait(handle, { for: 'ready' }); +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:wait-for-l2}} +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:wait-for-ready}} ``` ## Finalization (required step) ```ts -const result = await sdk.withdrawals.finalize(handle.l2TxHash); -console.log('Finalization result:', result); +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:wfinalize}} ``` ## Error handling patterns @@ -86,11 +90,7 @@ console.log('Finalization result:', result); **Exceptions** ```ts -try { - const handle = await sdk.withdrawals.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, optional revert) -} +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:try-catch-create}} ``` **No-throw style** @@ -99,19 +99,7 @@ Use `try*` methods to avoid exceptions. They return `{ ok, value }` or `{ ok, er 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); - } -} +{{#include ../../../snippets/ethers/guides/withdrawals-eth-guide.test.ts:tryCreate}} ``` ## Troubleshooting diff --git a/docs/src/guides/withdrawals/viem.md b/docs/src/guides/withdrawals/viem.md index 38206df..a56d1a6 100644 --- a/docs/src/guides/withdrawals/viem.md +++ b/docs/src/guides/withdrawals/viem.md @@ -26,11 +26,17 @@ Withdrawals are a **two-step process**: | `refundRecipient` | No | L2 address to receive fee refunds (if applicable) | | `l2TxOverrides` | No | L2 tx overrides (e.g. gasLimit, maxFeePerGas, maxPriorityFeePerGas) | - ## Fast path (one-shot) ```ts -{{#include ../../../snippets/viem/withdrawals-eth.ts}} +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:imports}} + +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:main}} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); ``` - `create()` prepares **and** sends the L2 withdrawal. @@ -45,7 +51,7 @@ Withdrawals are a **two-step process**: Preview fees/steps and whether extra approvals are required. ```ts -const quote = await sdk.withdrawals.quote(params); +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:quote}} ``` **2. Prepare (build txs, don’t send)** @@ -53,7 +59,7 @@ const quote = await sdk.withdrawals.quote(params); Get `TransactionRequest[]` for signing/UX. ```ts -const plan = await sdk.withdrawals.prepare(params); +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:prepare}} ``` **3. Create (send)** @@ -61,7 +67,7 @@ const plan = await sdk.withdrawals.prepare(params); Use defaults, or send your prepared txs if you customized. ```ts -const handle = await sdk.withdrawals.create(params); +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:create}} ``` ## Track progress (status vs wait) @@ -69,15 +75,13 @@ const handle = await sdk.withdrawals.create(params); **Non-blocking snapshot** ```ts -const s = await sdk.withdrawals.status(handle /* or l2TxHash */); -// 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:status}} ``` **Block until checkpoint** ```ts -const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' }); -await sdk.withdrawals.wait(handle, { for: 'ready' }); // becomes finalizable +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:wait}} ``` ## Finalization (required step) @@ -86,8 +90,7 @@ 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); +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:wfinalize}} ``` ## Error handling patterns @@ -95,11 +98,7 @@ console.log('Finalization status:', result.status.phase); **Exceptions** ```ts -try { - const handle = await sdk.withdrawals.create(params); -} catch (e) { - // normalized error envelope (type, operation, message, context, optional revert) -} +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:try-catch-create}} ``` **No-throw style** @@ -113,19 +112,7 @@ These never throwβ€”so you don’t need `try/catch`. Instead they return: 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); - } -} +{{#include ../../../snippets/viem/guides/withdrawals-eth-guide.test.ts:tryCreate}} ``` ## Troubleshooting diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index bd94625..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# ZKsync-JS - -Welcome! Use the sidebar for API reference. - -- **Core**: encoding, attributes, bundle builders. -- **Ethers**: high-level send helpers. -- **Viem**: diff --git a/docs/src/overview/adapters.md b/docs/src/overview/adapters.md index 11718e8..66d3a82 100644 --- a/docs/src/overview/adapters.md +++ b/docs/src/overview/adapters.md @@ -31,52 +31,21 @@ The SDK extends your existing client. Configure **viem** or **ethers** as you no ### viem (public + wallet client) ```ts -import { createPublicClient, createWalletClient, http, parseEther } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/core'; +{{#include ../../snippets/viem/overview/adapter.test.ts:viem-adapter-imports}} -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +{{#include ../../snippets/viem/overview/adapter-basic.test.ts:init-viem-adapter}} -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 +{{#include ../../snippets/viem/overview/adapter.test.ts:viem-deposit}} ``` ### ethers (providers + signer) ```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/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); +{{#include ../../snippets/ethers/overview/adapter.test.ts:ethers-adapter-imports}} -const params = { - amount: parseEther('0.01'), - to: await signer.getAddress(), - token: ETH_ADDRESS, -} as const; +{{#include ../../snippets/ethers/overview/adapter-basic.test.ts:init-ethers-adapter}} -const handle = await sdk.deposits.create(params); -await sdk.deposits.wait(handle, { for: 'l2' }); // funds available on L2 +{{#include ../../snippets/ethers/overview/adapter.test.ts:ethers-deposit}} ``` --- diff --git a/docs/src/overview/finalization.md b/docs/src/overview/finalization.md index 068d230..698ace2 100644 --- a/docs/src/overview/finalization.md +++ b/docs/src/overview/finalization.md @@ -49,21 +49,7 @@ Withdrawals are a **two-step process**: finalize-by-handle.ts ```ts -// 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 +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-short}} ``` @@ -75,28 +61,15 @@ console.log(receipt?.transactionHash); // L1 finalize tx hash // 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); +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-by-hash}} ``` - Prefer "no-throw" variants in UI/services that need explicit flow control. ```ts -const r = await sdk.withdrawals.tryFinalize(l2TxHash); -if (!r.ok) { - console.error('Finalize failed:', r.error); -} else { - console.log('Finalized on L1:', r.value.receipt?.transactionHash); -} +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-try-finalize}} ``` ## Operational Tips diff --git a/docs/src/overview/mental-model.md b/docs/src/overview/mental-model.md index 1b9426c..6888f00 100644 --- a/docs/src/overview/mental-model.md +++ b/docs/src/overview/mental-model.md @@ -80,24 +80,7 @@ For more robust error handling without `try/catch` blocks, **every core method h 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); -} +{{#include ../../snippets/ethers/overview/adapter.test.ts:mental-model}} ``` ➑️ **Best for:** Applications that prefer a functional error-handling pattern and want to avoid uncaught exceptions. @@ -111,11 +94,5 @@ These primitives allow you to compose flows that are as simple or as complex as 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!'); +{{#include ../../snippets/ethers/overview/adapter.test.ts:simple-flow}} ``` diff --git a/docs/src/overview/status-vs-wait.md b/docs/src/overview/status-vs-wait.md index 61d8913..76b949f 100644 --- a/docs/src/overview/status-vs-wait.md +++ b/docs/src/overview/status-vs-wait.md @@ -41,8 +41,7 @@ Use `status(...)` for UI refreshes; use `wait(...)` when you need to gate logic withdrawals-status.ts ```ts -const s = await sdk.withdrawals.status(handleOrHash); -// s.phase ∈ 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED' +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-status}} ``` @@ -67,14 +66,7 @@ const s = await sdk.withdrawals.status(handleOrHash); withdrawals-wait.ts ```ts -// 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 }); +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-wait}} ``` @@ -105,8 +97,7 @@ const l1Rcpt = await sdk.withdrawals.wait(handle, { for: 'finalized', timeoutMs: deposits-status.ts ```ts -const s = await sdk.deposits.status(handleOrL1Hash); -// s.phase ∈ 'UNKNOWN' | 'L1_PENDING' | 'L1_INCLUDED' | 'L2_PENDING' | 'L2_EXECUTED' | 'L2_FAILED' +{{#include ../../snippets/viem/overview/adapter.test.ts:deposit-status}} ``` @@ -124,8 +115,7 @@ const s = await sdk.deposits.status(handleOrL1Hash); deposits-wait.ts ```ts -const l1Rcpt = await sdk.deposits.wait(handle, { for: 'l1' }); -const l2Rcpt = await sdk.deposits.wait(handle, { for: 'l2' }); +{{#include ../../snippets/viem/overview/adapter.test.ts:deposit-wait}} ``` @@ -146,14 +136,7 @@ const l2Rcpt = await sdk.deposits.wait(handle, { for: 'l2' }); polling.ts ```ts -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 -} +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-poll}} ``` @@ -169,12 +152,7 @@ Prefer **no-throw** variants if you want explicit flow control: no-throw.ts ```ts -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); -} +{{#include ../../snippets/viem/overview/adapter.test.ts:withdraw-try-wait}} ``` diff --git a/docs/src/overview/what-it-does.md b/docs/src/overview/what-it-does.md deleted file mode 100644 index 14fc8fa..0000000 --- a/docs/src/overview/what-it-does.md +++ /dev/null @@ -1 +0,0 @@ -# What this SDK does diff --git a/docs/src/quickstart/choose-adapter.md b/docs/src/quickstart/choose-adapter.md index 713b94f..bfb5127 100644 --- a/docs/src/quickstart/choose-adapter.md +++ b/docs/src/quickstart/choose-adapter.md @@ -13,49 +13,28 @@ You can't make a wrong choice. Both adapters are fully supported and provide the The only difference in your code is the initial setup. **All subsequent SDK calls are identical.** -#### viem +### viem ```ts -import { createPublicClient, createWalletClient, http } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; +{{#include ../../snippets/viem/overview/adapter-basic.test.ts:viem-basic-imports}} -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); +{{#include ../../snippets/viem/overview/adapter-basic.test.ts:init-viem-adapter}} ``` -#### ethers +### ethers ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../snippets/ethers/overview/adapter-basic.test.ts:ethers-basic-imports}} -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); +{{#include ../../snippets/ethers/overview/adapter-basic.test.ts:init-ethers-adapter}} ``` -### Identical SDK Usage +## 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()); +{{#include ../../snippets/viem/overview/adapter.test.ts:deposit-quote}} ``` ## Conclusion diff --git a/docs/src/quickstart/ethers.md b/docs/src/quickstart/ethers.md index b0bc1f8..658a25d 100644 --- a/docs/src/quickstart/ethers.md +++ b/docs/src/quickstart/ethers.md @@ -26,8 +26,8 @@ Next, create a `.env` file in your project's root directory to store your privat 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" +L1_RPC=https://sepolia.infura.io/v3/YOUR_INFURA_ID +L2_RPC="ZKSYNC-OS-TESTNET-RPC" ``` ## 3. The Deposit Script @@ -38,96 +38,9 @@ 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 '@matterlabs/zksync-js/ethers'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/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, - // Optional: pin gas fees instead of using provider estimates - // l1TxOverrides: { - // gasLimit: 280_000n, - // maxFeePerGas: parseEther('0.00000002'), // 20 gwei - // maxPriorityFeePerGas: parseEther('0.000000002'), // 2 gwei - // }, - }; - - // 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); - */ -} +{{#include ../../snippets/ethers/quickstart/quickstart.test.ts:quickstart-imports}} + +{{#include ../../snippets/ethers/quickstart/quickstart.test.ts:quickstart-main}} main().catch((error) => { console.error('An error occurred:', error); diff --git a/docs/src/quickstart/viem.md b/docs/src/quickstart/viem.md index 234fb7e..8388ab0 100644 --- a/docs/src/quickstart/viem.md +++ b/docs/src/quickstart/viem.md @@ -25,8 +25,8 @@ Create an `.env` in your project root (never commit this): 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 +L1_RPC=https://sepolia.infura.io/v3/YOUR_INFURA_ID +L2_RPC=ZKSYNC-OS-TESTNET-RPC ``` ## 3. The Deposit Script @@ -35,96 +35,9 @@ 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 '@matterlabs/zksync-js/viem'; -import { ETH_ADDRESS } from '@matterlabs/zksync-js/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, - // Optional gas control: - // l1TxOverrides: { - // gasLimit: 280_000n, - // maxFeePerGas: parseEther('0.00000002'), - // maxPriorityFeePerGas: parseEther('0.000000002'), - // }, - }; - - // 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); - */ -} +{{#include ../../snippets/viem/quickstart/quickstart.test.ts:quickstart-imports}} + +{{#include ../../snippets/viem/quickstart/quickstart.test.ts:quickstart-main}} main().catch((error) => { console.error('An error occurred:', error); diff --git a/docs/src/sdk-reference/core/errors.md b/docs/src/sdk-reference/core/errors.md index f67db31..c10608d 100644 --- a/docs/src/sdk-reference/core/errors.md +++ b/docs/src/sdk-reference/core/errors.md @@ -21,31 +21,9 @@ When the SDK throws, it throws an instance of `ZKsyncError`. Use `isZKsyncError(e)` to narrow and read the **error envelope**. ```ts -import { isZKsyncError } from '@matterlabs/zksync-js/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; - - switch (type) { - case 'VALIDATION': - case 'STATE': - // user/action fixable (bad input, not-ready, etc.) - break; - case 'EXECUTION': - case 'RPC': - // network/tx/provider issues - break; - } - - console.error(JSON.stringify(err.toJSON())); // structured log - } else { - throw e; // non-SDK error - } -} +{{#include ../../../snippets/core/errors.test.ts:error-import}} + +{{#include ../../../snippets/core/errors.test.ts:zksync-error}} ``` ## Envelope Shape @@ -59,34 +37,7 @@ try { ### `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; -}; +{{#include ../../../snippets/core/errors.test.ts:envelope-type}} ``` ### Categories (When to Expect Them) @@ -106,13 +57,7 @@ type ErrorEnvelope = { 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 { - console.log('l2TxHash', res.value.l2TxHash); -} +{{#include ../../../snippets/core/errors.test.ts:try-create}} ``` This is especially useful for **UI flows** where you want inline validation/state messages without `try/catch`. @@ -122,14 +67,7 @@ This is especially useful for **UI flows** where you want inline validation/stat If the provider exposes revert data, the adapters 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' - } -} +{{#include ../../../snippets/core/errors.test.ts:revert-details}} ``` **Notes** @@ -144,21 +82,12 @@ try { Ethers ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; -import { isZKsyncError } from '@matterlabs/zksync-js/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); +{{#include ../../../snippets/ethers/overview/adapter-basic.test.ts:ethers-basic-imports}} +{{#include ../../../snippets/core/errors.test.ts:error-import}} -const client = createEthersClient({ l1, l2, signer }); -const sdk = createEthersSdk(client); +{{#include ../../../snippets/ethers/overview/adapter-basic.test.ts:init-ethers-adapter}} -const res = await sdk.deposits.tryCreate({ token, amount, to }); -if (!res.ok) { - console.error(res.error.envelope); // structured envelope -} +{{#include ../../../snippets/core/errors.test.ts:envelope-error}} ``` @@ -167,28 +96,12 @@ if (!res.ok) { Viem ```ts -import { createPublicClient, http, createWalletClient, privateKeyToAccount } from 'viem'; -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; -import { isZKsyncError } from '@matterlabs/zksync-js/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 = createWalletClient({ account, transport: http(process.env.ETH_RPC!) }); -const l2Wallet = createWalletClient({ account, transport: http(process.env.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; - } -} +{{#include ../../../snippets/ethers/overview/adapter.test.ts:ethers-adapter-imports}} +{{#include ../../../snippets/core/errors.test.ts:error-import}} + +{{#include ../../../snippets/ethers/overview/adapter-basic.test.ts:init-ethers-adapter}} + +{{#include ../../../snippets/core/errors.test.ts:envelope-error}} ``` diff --git a/docs/src/sdk-reference/core/rpc.md b/docs/src/sdk-reference/core/rpc.md index 4a3f340..371b5bd 100644 --- a/docs/src/sdk-reference/core/rpc.md +++ b/docs/src/sdk-reference/core/rpc.md @@ -11,14 +11,7 @@ For standard Ethereum JSON-RPC (e.g., `eth_call`, `eth_getLogs`, `eth_getBalance ## zks_ Interface ```ts -interface ZksRpc { - getBridgehubAddress(): Promise
; - getBytecodeSupplierAddress(): Promise
; - getL2ToL1LogProof(txHash: Hex, index: number): Promise; - getReceiptWithL2ToL1(txHash: Hex): Promise; - getBlockMetadataByNumber(blockNumber: number): Promise; - getGenesis(): Promise; -} +{{#include ../../../snippets/core/rpc.test.ts:zks-rpc}} ``` --- @@ -30,7 +23,7 @@ interface ZksRpc { Fetch the on-chain **Bridgehub** contract address. ```ts -const addr = await client.zks.getBridgehubAddress(); +{{#include ../../../snippets/core/rpc.test.ts:bridgehub-address}} ``` --- @@ -40,7 +33,7 @@ const addr = await client.zks.getBridgehubAddress(); Fetch the on-chain **Bytecode Supplier** contract address. ```ts -const addr = await client.zks.getBytecodeSupplierAddress(); +{{#include ../../../snippets/core/rpc.test.ts:bytecode-supplier}} ``` --- @@ -57,14 +50,7 @@ Return a normalized proof for the **L2β†’L1 log** at `index` in `txHash`. | `index` | number | yes | Zero-based index of the target L2β†’L1 log within the tx. | ```ts -const proof = await client.zks.getL2ToL1LogProof(l2TxHash, 0); -/* -{ - id: bigint, - batchNumber: bigint, - proof: Hex[] -} -*/ +{{#include ../../../snippets/viem/overview/adapter.test.ts:log-proof}} ``` > [!INFO] @@ -78,8 +64,7 @@ const proof = await client.zks.getL2ToL1LogProof(l2TxHash, 0); 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 +{{#include ../../../snippets/viem/overview/adapter.test.ts:receipt-with-logs}} ``` --- @@ -94,49 +79,18 @@ Price fields are returned as `bigint`. **Example** ```ts -const meta = await client.zks.getBlockMetadataByNumber(123_456); -if (meta) { - console.log(meta.pubdataPricePerByte, meta.nativePrice, meta.executionVersion); -} +{{#include ../../../snippets/core/rpc.test.ts:block-metadata}} ``` **Returns** ```ts -type BlockMetadata = { - pubdataPricePerByte: bigint; - nativePrice: bigint; - executionVersion: number; -}; +{{#include ../../../snippets/core/rpc.test.ts:metadata-type}} ``` --- -## Types (overview) - -```ts -type ProofNormalized = { - id: bigint; - batchNumber: bigint; - proof: Hex[]; - root: Hex; -}; - -type ReceiptWithL2ToL1 = { - // …standard receipt fields… - l2ToL1Logs: unknown[]; -}; - -type BlockMetadata = { - pubdataPricePerByte: bigint; - nativePrice: bigint; - executionVersion: number; -}; -``` - ---- - -## `getGenesis()` +### `getGenesis()` **What it does** Retrieves the L2 genesis configuration exposed by the node, including initial contract deployments, storage patches, execution version, and the expected genesis root. @@ -144,31 +98,27 @@ Retrieves the L2 genesis configuration exposed by the node, including initial co **Example** ```ts -const genesis = await client.zks.getGenesis(); +{{#include ../../../snippets/core/rpc.test.ts:genesis-method}} +``` -for (const contract of genesis.initialContracts) { - console.log('Contract at', contract.address, 'with bytecode', contract.bytecode); -} +**Returns** -console.log('Execution version:', genesis.executionVersion); -console.log('Genesis root:', genesis.genesisRoot); +```ts +{{#include ../../../snippets/core/rpc.test.ts:genesis-type}} ``` -**Returns** +--- + +## Types (overview) ```ts -type GenesisInput = { - initialContracts: { - address: Address; - bytecode: `0x${string}`; - }[]; - additionalStorage: { - key: `0x${string}`; - value: `0x${string}`; - }[]; - executionVersion: number; - genesisRoot: `0x${string}`; -}; +{{#include ../../../snippets/core/rpc.test.ts:zks-rpc}} + +{{#include ../../../snippets/core/rpc.test.ts:proof-receipt-type}} + +{{#include ../../../snippets/core/rpc.test.ts:metadata-type}} + +{{#include ../../../snippets/core/rpc.test.ts:genesis-type}} ``` --- @@ -179,17 +129,12 @@ type GenesisInput = { Ethers ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient } from '@matterlabs/zksync-js/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); +{{#include ../../../snippets/ethers/overview/adapter-basic.test.ts:ethers-basic-imports}} -const client = createEthersClient({ l1, l2, signer }); +{{#include ../../../snippets/ethers/overview/adapter-basic.test.ts:init-ethers-adapter}} // Public RPC surface: -const bridgehub = await client.zks.getBridgehubAddress(); +{{#include ../../../snippets/core/rpc.test.ts:bridgehub-address}} ``` @@ -198,19 +143,12 @@ const bridgehub = await client.zks.getBridgehubAddress(); Viem ```ts -import { createPublicClient, http } from 'viem'; -import { createViemClient } from '@matterlabs/zksync-js/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 */; +{{#include ../../../snippets/viem/overview/adapter.test.ts:viem-adapter-imports}} -const client = createViemClient({ l1, l2, l1Wallet }); +{{#include ../../../snippets/viem/overview/adapter-basic.test.ts:init-viem-adapter}} // Public RPC surface: -const bridgehub = await client.zks.getBridgehubAddress(); +{{#include ../../../snippets/core/rpc.test.ts:bridgehub-address}} ``` diff --git a/docs/src/sdk-reference/ethers/client.md b/docs/src/sdk-reference/ethers/client.md index 88dc8ad..3be696c 100644 --- a/docs/src/sdk-reference/ethers/client.md +++ b/docs/src/sdk-reference/ethers/client.md @@ -14,26 +14,16 @@ Carries providers/signer, resolves core contract addresses, and exposes connecte ## Import ```ts -import { createEthersClient } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/client.test.ts:client-import}} ``` ## Quick Start ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/client.test.ts:ethers-import}} +{{#include ../../../snippets/ethers/reference/client.test.ts:client-import}} -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(); +{{#include ../../../snippets/ethers/reference/client.test.ts:init-client}} ``` > [!TIP] @@ -69,13 +59,7 @@ const { bridgehub, l1AssetRouter } = await client.contracts(); 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 -} -*/ +{{#include ../../../snippets/ethers/reference/client.test.ts:ensureAddresses}} ``` ### `contracts() β†’ Promise<{ ...contracts }>` @@ -83,9 +67,7 @@ const a = await client.ensureAddresses(); Return connected `ethers.Contract` instances for all core contracts. ```ts -const c = await client.contracts(); -const bh = c.bridgehub; -await bh.getAddress(); +{{#include ../../../snippets/ethers/reference/client.test.ts:contracts}} ``` ### `refresh(): void` @@ -93,8 +75,7 @@ await bh.getAddress(); Clear cached addresses/contracts. Subsequent calls re-resolve. ```ts -client.refresh(); -await client.ensureAddresses(); +{{#include ../../../snippets/ethers/reference/client.test.ts:refresh}} ``` ### `baseToken(chainId: bigint) β†’ Promise
` @@ -102,7 +83,7 @@ await client.ensureAddresses(); Return the **L1 base-token address** for a given L2 chain via `Bridgehub.baseToken(chainId)`. ```ts -const base = await client.baseToken(324n); +{{#include ../../../snippets/ethers/reference/client.test.ts:base}} ``` ## Types @@ -110,15 +91,7 @@ const base = await client.baseToken(324n); ### `ResolvedAddresses` ```ts -type ResolvedAddresses = { - bridgehub: Address; - l1AssetRouter: Address; - l1Nullifier: Address; - l1NativeTokenVault: Address; - l2AssetRouter: Address; - l2NativeTokenVault: Address; - l2BaseTokenSystem: Address; -}; +{{#include ../../../snippets/ethers/reference/client.test.ts:resolved-type}} ``` ## Notes & Pitfalls diff --git a/docs/src/sdk-reference/ethers/contracts.md b/docs/src/sdk-reference/ethers/contracts.md index c58c36e..99af9ca 100644 --- a/docs/src/sdk-reference/ethers/contracts.md +++ b/docs/src/sdk-reference/ethers/contracts.md @@ -13,16 +13,9 @@ Resolved addresses and connected core contracts for the Ethers adapter. ## Import ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/contracts.test.ts:imports}} -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.contracts β†’ ContractsResource +{{#include ../../../snippets/ethers/reference/contracts.test.ts:init-sdk}} ``` ## Quick Start @@ -30,10 +23,7 @@ const sdk = createEthersSdk(client); Resolve addresses and contract handles: ```ts -const addresses = await sdk.contracts.addresses(); -const { l1NativeTokenVault, l2AssetRouter } = await sdk.contracts.instances(); - -const ntv = await sdk.contracts.l1NativeTokenVault(); +{{#include ../../../snippets/ethers/reference/contracts.test.ts:ntv}} ``` ## Method Reference @@ -43,18 +33,7 @@ const ntv = await sdk.contracts.l1NativeTokenVault(); Resolve core addresses (Bridgehub, routers, vaults, base-token system). ```ts -const a = await sdk.contracts.addresses(); -/* -{ - bridgehub, - l1AssetRouter, - l1Nullifier, - l1NativeTokenVault, - l2AssetRouter, - l2NativeTokenVault, - l2BaseTokenSystem -} -*/ +{{#include ../../../snippets/ethers/reference/contracts.test.ts:addresses}} ``` ### `instances() β†’ Promise<{ ...contracts }>` @@ -62,18 +41,7 @@ const a = await sdk.contracts.addresses(); Return connected `ethers.Contract` instances for all core contracts. ```ts -const c = await sdk.contracts.instances(); -/* -{ - bridgehub, - l1AssetRouter, - l1Nullifier, - l1NativeTokenVault, - l2AssetRouter, - l2NativeTokenVault, - l2BaseTokenSystem -} -*/ +{{#include ../../../snippets/ethers/reference/contracts.test.ts:instances}} ``` ### One-off Contract Getters @@ -89,7 +57,7 @@ const c = await sdk.contracts.instances(); | `l2BaseTokenSystem()` | `Promise` | Connected L2 Base Token System. | ```ts -const router = await sdk.contracts.l2AssetRouter(); +{{#include ../../../snippets/ethers/reference/contracts.test.ts:router}} ``` ## Notes & Pitfalls diff --git a/docs/src/sdk-reference/ethers/deposits.md b/docs/src/sdk-reference/ethers/deposits.md index 7d9b4b8..98d7388 100644 --- a/docs/src/sdk-reference/ethers/deposits.md +++ b/docs/src/sdk-reference/ethers/deposits.md @@ -15,16 +15,9 @@ L1 β†’ L2 deposits for ETH and ERC-20 tokens with quote, prepare, create, status ## Import ```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/deposits.test.ts:imports}} -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 +{{#include ../../../snippets/ethers/reference/deposits.test.ts:init-sdk}} ``` ## Quick Start @@ -32,13 +25,9 @@ const sdk = createEthersSdk(client); 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(), -}); +{{#include ../../../snippets/ethers/reference/deposits.test.ts:eth-import}} -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); // null only if no L1 hash +{{#include ../../../snippets/ethers/reference/deposits.test.ts:create-deposit}} ``` > [!TIP] @@ -78,32 +67,7 @@ Estimate the operation (route, approvals, gas hints). Does **not** send transact **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", - summary: { - route, - approvalsNeeded: [{ token, spender, amount }], - amounts: { - transfer: { token, amount } - }, - fees: { - token, - maxTotal, - mintValue, - l1: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, maxTotal }, - l2: { total, baseCost, operatorTip, gasLimit, maxFeePerGas, maxPriorityFeePerGas, gasPerPubdata } - }, - baseCost, - mintValue - } -} -*/ +{{#include ../../../snippets/ethers/reference/deposits.test.ts:quote-deposit}} ``` > [!TIP] @@ -120,17 +84,7 @@ Build the plan (ordered steps + unsigned transactions) 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 } - ] -} -*/ +{{#include ../../../snippets/ethers/reference/deposits.test.ts:plan-deposit}} ``` ### `tryPrepare(p) β†’ Promise<{ ok: true; value: DepositPlan } | { ok: false; error }>` @@ -145,15 +99,7 @@ Returns a handle with the L1 transaction 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 -} -*/ +{{#include ../../../snippets/ethers/reference/deposits.test.ts:handle}} ``` > [!WARNING] @@ -181,8 +127,7 @@ Accepts either the `DepositHandle` from `create()` or a raw L1 transaction hash. | `L2_FAILED` | L2 receipt found with `status !== 1` | ```ts -const s = await sdk.deposits.status(handle); -// { phase, l1TxHash, l2TxHash? } +{{#include ../../../snippets/ethers/reference/deposits.test.ts:status}} ``` ### `wait(handleOrHash, { for: 'l1' | 'l2' }) β†’ Promise` @@ -193,8 +138,7 @@ Block until the specified checkpoint. * `{ 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' }); +{{#include ../../../snippets/ethers/reference/deposits.test.ts:wait}} ``` ### `tryWait(handleOrHash, opts) β†’ Result` @@ -206,105 +150,48 @@ Result-style `wait`. ### ETH Deposit (Typical) ```ts -const handle = await sdk.deposits.create({ - token: ETH_ADDRESS, - amount: parseEther('0.001'), - to: await signer.getAddress(), -}); - -await sdk.deposits.wait(handle, { for: 'l2' }); +{{#include ../../../snippets/ethers/reference/deposits.test.ts:create-eth-deposit}} ``` ### ERC-20 Deposit ```ts -const handle = await sdk.deposits.create({ - token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Example: USDC - amount: 1_000_000n, // 1.0 USDC (6 decimals) - to: await signer.getAddress(), -}); - -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +{{#include ../../../snippets/ethers/reference/deposits.test.ts:token-address}} +{{#include ../../../snippets/ethers/reference/deposits.test.ts:create-token-deposit}} ``` --- ## Types (Overview) +### Deposit Params + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:params-type}} +``` + +### Deposit Quote + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:quote-type}} +``` + +### Deposit Plan + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:plan-type}} +``` + +### Deposit Waitable + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:wait-type}} +``` + +### Deposit Status + ```ts -type DepositParams = { - token: Address; // 0x…00 for ETH - amount: bigint; // wei - to?: Address; // L2 recipient - refundRecipient?: Address; - l2GasLimit?: bigint; - gasPerPubdata?: bigint; - operatorTip?: bigint; - l1TxOverrides?: Eip1559GasOverrides; -}; - -type Eip1559GasOverrides = { - gasLimit?: bigint; - maxFeePerGas?: bigint; - maxPriorityFeePerGas?: bigint; -}; - -type DepositQuote = { - route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; - summary: { - route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; - approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; - amounts: { - transfer: { - token: Address; - amount: bigint; - }; - }; - fees: { - token: Address; - maxTotal: bigint; - mintValue: bigint; - l1: { - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas: bigint; - maxTotal: bigint; - }; - l2: { - total: bigint; - baseCost: bigint; - operatorTip: bigint; - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas: bigint; - gasPerPubdata: bigint; - }; - }; - baseCost: bigint; - mintValue: 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 }; +{{#include ../../../snippets/ethers/reference/deposits.test.ts:status-type}} ``` > [!TIP] diff --git a/docs/src/sdk-reference/ethers/finalization-services.md b/docs/src/sdk-reference/ethers/finalization-services.md index c91f44b..9dbbc8b 100644 --- a/docs/src/sdk-reference/ethers/finalization-services.md +++ b/docs/src/sdk-reference/ethers/finalization-services.md @@ -17,49 +17,15 @@ These utilities fetch the required L2β†’L1 proof data, check readiness, and subm ## Import & Setup ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { - createEthersClient, - createEthersSdk, - createFinalizationServices -} from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/finalization-service.test.ts:imports}} -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 }); -// optional: const sdk = createEthersSdk(client); - -const svc = createFinalizationServices(client); +{{#include ../../../snippets/ethers/reference/finalization-service.test.ts:init-sdk}} ``` ## Minimal Usage Example ```ts -const l2TxHash: Hex = '0x...'; - -// 1) Build finalize params + discover the L1 Nullifier to call -const { params, nullifier } = await svc.fetchFinalizeDepositParams(l2TxHash); - -// 2) (Optional) check finalization -const already = await svc.isWithdrawalFinalized(params); -if (already) { - console.log('Already finalized on L1'); -} else { - // 3) Dry-run on L1 to confirm readiness (no gas spent) - const readiness = await svc.simulateFinalizeReadiness(params, nullifier); - - if (readiness.kind === 'READY') { - // 4) Submit finalize tx - const { hash, wait } = await svc.finalizeDeposit(params, nullifier); - console.log('L1 finalize tx:', hash); - const rcpt = await wait(); - console.log('Finalized in block:', rcpt.blockNumber); - } else { - console.warn('Not ready to finalize:', readiness); - } -} +{{#include ../../../snippets/ethers/reference/finalization-service.test.ts:finalize-with-svc}} ``` > [!TIP] @@ -156,77 +122,9 @@ If you are also using `sdk.withdrawals.status(...)`, the phases align conceptual ## Types ```ts -// Finalize call input -export interface FinalizeDepositParams { - chainId: bigint; - l2BatchNumber: bigint; - l2MessageIndex: bigint; - l2Sender: Address; - l2TxNumberInBatch: number; - message: Hex; - merkleProof: Hex[]; -} - -// Key that identifies a withdrawal in the Nullifier mapping -export type WithdrawalKey = { - chainIdL2: bigint; - l2BatchNumber: bigint; - l2MessageIndex: bigint; -}; - -// Overall withdrawal state (used by higher-level status helpers) -type WithdrawalPhase = - | 'L2_PENDING' - | 'L2_INCLUDED' - | 'PENDING' - | 'READY_TO_FINALIZE' - | 'FINALIZING' - | 'FINALIZED' - | 'FINALIZE_FAILED' - | 'UNKNOWN'; - -export type WithdrawalStatus = { - phase: WithdrawalPhase; - l2TxHash: Hex; - l1FinalizeTxHash?: Hex; - key?: WithdrawalKey; -}; - -// Readiness result returned by simulateFinalizeReadiness(...) -export type FinalizeReadiness = - | { kind: 'READY' } - | { kind: 'FINALIZED' } - | { - kind: 'NOT_READY'; - // temporary, retry later - reason: 'paused' | 'batch-not-executed' | 'root-missing' | 'unknown'; - detail?: string; - } - | { - kind: 'UNFINALIZABLE'; - // permanent, won’t become ready - reason: 'message-invalid' | 'invalid-chain' | 'settlement-layer' | 'unsupported'; - detail?: string; - }; - -// Ethers-bound service surface -export interface FinalizationServices { - fetchFinalizeDepositParams( - l2TxHash: Hex, - ): Promise<{ params: FinalizeDepositParams; nullifier: Address }>; - - isWithdrawalFinalized(key: WithdrawalKey): Promise; - - simulateFinalizeReadiness( - params: FinalizeDepositParams, - nullifier: Address, - ): Promise; - - finalizeDeposit( - params: FinalizeDepositParams, - nullifier: Address, - ): Promise<{ hash: string; wait: () => Promise }>; -} +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:status-type}} + +{{#include ../../../snippets/ethers/reference/finalization-service.test.ts:finalization-types}} ``` --- diff --git a/docs/src/sdk-reference/ethers/sdk.md b/docs/src/sdk-reference/ethers/sdk.md index 8053281..2d1ae9e 100644 --- a/docs/src/sdk-reference/ethers/sdk.md +++ b/docs/src/sdk-reference/ethers/sdk.md @@ -13,37 +13,21 @@ High-level SDK built on top of the **Ethers adapter** β€” provides deposits, wit ## Import ```ts -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/sdk.test.ts:sdk-import}} ``` ## Quick Start ```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/sdk.test.ts:ethers-import}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:eth-import}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:sdk-import}} -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); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:init-sdk}} -const client = createEthersClient({ l1, l2, signer }); -const sdk = createEthersSdk(client); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:erc-20-address}} -// Example: deposit 0.05 ETH L1 β†’ L2 and 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.contracts.instances(); - -// Example: map a token L1 β†’ L2 -const token = await sdk.tokens.resolve('0xYourToken'); -console.log(token.l2); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:basic-sdk}} ``` > [!TIP] @@ -90,7 +74,7 @@ Utilities for resolved addresses and connected contracts. Token mapping lives in Resolve core addresses (Bridgehub, routers, vaults, base-token system). ```ts -const a = await sdk.contracts.addresses(); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:contract-addresses}} ``` ### `instances() β†’ Promise<{ ...contracts }>` @@ -98,7 +82,7 @@ const a = await sdk.contracts.addresses(); Return connected `ethers.Contract` instances for all core contracts. ```ts -const c = await sdk.contracts.instances(); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:contract-instances}} ``` ### One-off Contract Getters @@ -114,7 +98,7 @@ const c = await sdk.contracts.instances(); | `l2BaseTokenSystem()` | `Promise` | Connected L2 Base Token System. | ```ts -const nullifier = await sdk.contracts.l1Nullifier(); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:nullifier}} ``` ## Notes & Pitfalls diff --git a/docs/src/sdk-reference/ethers/tokens.md b/docs/src/sdk-reference/ethers/tokens.md index ce4d7f6..90628d3 100644 --- a/docs/src/sdk-reference/ethers/tokens.md +++ b/docs/src/sdk-reference/ethers/tokens.md @@ -14,15 +14,10 @@ Token identity, L1↔L2 mapping, bridge asset IDs, and chain token facts for ETH ## Import ```ts -import { JsonRpcProvider, Wallet } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/sdk.test.ts:ethers-import}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:sdk-import}} -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); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:init-sdk}} // sdk.tokens β†’ TokensResource ``` @@ -31,34 +26,22 @@ const sdk = createEthersSdk(client); Resolve a token by L1 address and fetch its L2 counterpart + bridge metadata: ```ts -const token = await sdk.tokens.resolve('0xYourTokenL1...'); -/* -{ - kind: 'eth' | 'base' | 'erc20', - l1: Address, - l2: Address, - assetId: Hex, - originChainId: bigint, - isChainEthBased: boolean, - baseTokenAssetId: Hex, - wethL1: Address, - wethL2: Address, -} -*/ +{{#include ../../../snippets/ethers/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:resolve-token}} ``` Map addresses directly: ```ts -const l2Addr = await sdk.tokens.toL2Address('0xTokenL1...'); -const l1Addr = await sdk.tokens.toL1Address(l2Addr); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:map-token}} ``` Compute bridge identifiers: ```ts -const assetId = await sdk.tokens.assetIdOfL1('0xTokenL1...'); -const backL2 = await sdk.tokens.l2TokenFromAssetId(assetId); +{{#include ../../../snippets/ethers/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/ethers/reference/sdk.test.ts:token-asset-ids}} ``` ## Method Reference diff --git a/docs/src/sdk-reference/ethers/withdrawals.md b/docs/src/sdk-reference/ethers/withdrawals.md index 4ee3048..ae5a809 100644 --- a/docs/src/sdk-reference/ethers/withdrawals.md +++ b/docs/src/sdk-reference/ethers/withdrawals.md @@ -15,16 +15,9 @@ L2 β†’ L1 withdrawals for ETH and ERC-20 tokens with quote, prepare, create, sta ## Import ```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk } from '@matterlabs/zksync-js/ethers'; +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:imports}} -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 +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:init-sdk}} ``` ## Quick Start @@ -32,20 +25,9 @@ const sdk = createEthersSdk(client); 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 }); +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:eth-import}} -// 3) Finalize on L1 (no-op if already finalized) -const { status, receipt: l1Receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:create-withdrawal}} ``` > [!INFO] @@ -81,25 +63,7 @@ Estimate the operation (route, approvals, gas hints). Does **not** send transact **Returns:** `WithdrawQuote` ```ts -const q = await sdk.withdrawals.quote({ token, amount, to }); -/* -{ - route: "base" | "erc20-nonbase", - summary: { - route, - approvalsNeeded: [{ token, spender, amount }], - amounts: { - transfer: { token, amount } - }, - fees: { - token, - maxTotal, - mintValue, - l2: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, total } - } - } -} -*/ +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:quote}} ``` ### `tryQuote(p) β†’ Promise<{ ok: true; value: WithdrawQuote } | { ok: false; error }>` @@ -113,17 +77,7 @@ Build the plan (ordered L2 steps + unsigned transactions) without sending. **Returns:** `WithdrawPlan` ```ts -const plan = await sdk.withdrawals.prepare({ token, amount, to }); -/* -{ - route, - summary: WithdrawQuote, - steps: [ - { key, kind, tx: TransactionRequest }, - // … - ] -} -*/ +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:plan}} ``` ### `tryPrepare(p) β†’ Promise<{ ok: true; value: WithdrawPlan } | { ok: false; error }>` @@ -138,15 +92,7 @@ Returns a handle containing the **L2 transaction hash**. **Returns:** `WithdrawHandle` ```ts -const handle = await sdk.withdrawals.create({ token, amount, to }); -/* -{ - kind: "withdrawal", - l2TxHash: Hex, - stepHashes: Record, - plan: WithdrawPlan -} -*/ +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:handle}} ``` > [!WARNING] @@ -173,8 +119,7 @@ Accepts either a `WithdrawHandle` or a raw **L2 transaction hash**. | `FINALIZED` | Already finalized on L1 | ```ts -const s = await sdk.withdrawals.status(handle); -// { phase, l2TxHash, key? } +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:status}} ``` ### `wait(handleOrHash, { for: 'l2' | 'ready' | 'finalized', pollMs?, timeoutMs? })` @@ -186,9 +131,8 @@ Block until a target phase is reached. * `{ 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 }); +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:receipt-1}} +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:receipt-2}} ``` > [!TIP] @@ -205,10 +149,7 @@ Send the **L1 finalize** transaction β€” **only if ready**. If already finalized, returns the current status without sending. ```ts -const { status, receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); -if (status.phase === 'FINALIZED') { - console.log('L1 tx:', receipt?.transactionHash); -} +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:finalize}} ``` > [!INFO] @@ -224,87 +165,41 @@ Result-style `finalize`. ### Minimal Happy Path ```ts -const handle = await sdk.withdrawals.create({ token, amount, to }); +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:min-happy-path}} +``` -// L2 inclusion -await sdk.withdrawals.wait(handle, { for: 'l2' }); +--- -// Option A: finalize immediately (will throw if not ready) -await sdk.withdrawals.finalize(handle.l2TxHash); +## Types (Overview) -// Option B: wait for readiness, then finalize -await sdk.withdrawals.wait(handle, { for: 'ready' }); -await sdk.withdrawals.finalize(handle.l2TxHash); +### Withdraw Params + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:params-type}} ``` ---- +### Withdraw Quote -## Types (Overview) +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:quote-type}} +``` + +### Withdraw Plan + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:plan-type}} +``` + +### Withdraw Waitable + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:wait-type}} +``` + +### Withdraw Status ```ts -export interface WithdrawParams { - token: Address; // L2 token (ETH sentinel supported) - amount: bigint; // wei - to?: Address; // L1 recipient - l2GasLimit?: bigint; - l2TxOverrides?: Eip1559GasOverrides; -} - -export interface Eip1559GasOverrides { - gasLimit?: bigint; - maxFeePerGas?: bigint; - maxPriorityFeePerGas?: bigint; -} - -export interface WithdrawQuote { - route: 'base' | 'erc20-nonbase'; - summary: { - route: 'base' | 'erc20-nonbase'; - approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; - amounts: { - transfer: { - token: Address; - amount: bigint; - }; - }; - fees: { - token: Address; - maxTotal: bigint; - mintValue?: bigint; - l2?: { - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas?: bigint; - total: bigint; - }; - }; - }; -} - -export interface WithdrawPlan { - route: WithdrawQuote['route']; - summary: WithdrawQuote; - steps: Array<{ key: string; kind: string; tx: TTx }>; -} - -export interface WithdrawHandle { - kind: 'withdrawal'; - l2TxHash: Hex; - stepHashes: Record; - plan: WithdrawPlan; -} - -export 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' }) -export type TransactionReceiptZKsyncOS = TransactionReceipt & { - l2ToL1Logs?: Array; -}; +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:status-type}} ``` --- diff --git a/docs/src/sdk-reference/index.md b/docs/src/sdk-reference/index.md index e96ab67..af74e34 100644 --- a/docs/src/sdk-reference/index.md +++ b/docs/src/sdk-reference/index.md @@ -17,28 +17,15 @@ The **zksync-js** provides lightweight adapters for **ethers** and **viem** to b Ethers Example ```ts -import { JsonRpcProvider, Wallet, parseEther } from 'ethers'; -import { createEthersClient, createEthersSdk, ETH_ADDRESS } from '@matterlabs/zksync-js/ethers'; +{{#include ../../snippets/ethers/reference/sdk.test.ts:ethers-import}} +{{#include ../../snippets/ethers/reference/sdk.test.ts:eth-import}} +{{#include ../../snippets/ethers/reference/sdk.test.ts:sdk-import}} -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); +{{#include ../../snippets/ethers/reference/sdk.test.ts:init-sdk}} -// Low-level client + high-level SDK -const client = createEthersClient({ l1, l2, signer }); -const sdk = createEthersSdk(client); +{{#include ../../snippets/ethers/reference/sdk.test.ts:erc-20-address}} -// Deposit 0.05 ETH L1 β†’ L2 and wait for L2 execution -const handle = await sdk.deposits.create({ - token: ETH_ADDRESS, - amount: parseEther('0.001'), - 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(); +{{#include ../../snippets/ethers/reference/sdk.test.ts:basic-sdk}} ``` @@ -47,33 +34,15 @@ const bridgehub = await client.zks.getBridgehubAddress(); Viem Example ```ts -import { - createPublicClient, - http, - createWalletClient, - privateKeyToAccount, - parseEther, -} from 'viem'; -import { createViemClient, createViemSdk, ETH_ADDRESS } from '@matterlabs/zksync-js/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.001'), - 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(); +{{#include ../../snippets/viem/reference/sdk.test.ts:sdk-import}} +{{#include ../../snippets/viem/reference/sdk.test.ts:viem-import}} +{{#include ../../snippets/viem/reference/sdk.test.ts:eth-import}} + +{{#include ../../snippets/viem/reference/sdk.test.ts:init-sdk}} + +{{#include ../../snippets/viem/reference/sdk.test.ts:erc-20-address}} + +{{#include ../../snippets/viem/reference/sdk.test.ts:basic-sdk}} ``` diff --git a/docs/src/sdk-reference/viem/client.md b/docs/src/sdk-reference/viem/client.md index 225154a..aa0031b 100644 --- a/docs/src/sdk-reference/viem/client.md +++ b/docs/src/sdk-reference/viem/client.md @@ -14,37 +14,16 @@ Provides cached core contract addresses, typed contract access, convenience wall ## Import ```ts -import { createViemClient } from '@matterlabs/zksync-js/viem'; +{{#include ../../../snippets/viem/reference/client.test.ts:client-import}} ``` ## Quick Start ```ts -import { createPublicClient, createWalletClient, http } from 'viem'; +{{#include ../../../snippets/viem/reference/client.test.ts:client-import}} +{{#include ../../../snippets/viem/reference/client.test.ts:viem-import}} -// 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, e.g., 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(); +{{#include ../../../snippets/viem/reference/client.test.ts:init-client}} ``` > [!TIP] @@ -84,13 +63,7 @@ const { bridgehub, l1AssetRouter } = await client.contracts(); Resolve and cache core contract addresses from chain state (merging any provided overrides). ```ts -const a = await client.ensureAddresses(); -/* -{ - bridgehub, l1AssetRouter, l1Nullifier, l1NativeTokenVault, - l2AssetRouter, l2NativeTokenVault, l2BaseTokenSystem -} -*/ +{{#include ../../../snippets/viem/reference/client.test.ts:ensureAddresses}} ``` ### `contracts() β†’ Promise<{ ...contracts }>` @@ -98,8 +71,7 @@ const a = await client.ensureAddresses(); 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.* +{{#include ../../../snippets/viem/reference/client.test.ts:contracts}} ``` ### `refresh(): void` @@ -108,8 +80,7 @@ Clear cached addresses and contracts. Subsequent calls to `ensureAddresses()` or `contracts()` will re-resolve. ```ts -client.refresh(); -await client.ensureAddresses(); +{{#include ../../../snippets/viem/reference/client.test.ts:refresh}} ``` ### `baseToken(chainId: bigint) β†’ Promise
` @@ -117,7 +88,7 @@ await client.ensureAddresses(); Return the **L1 base-token address** for a given L2 chain via `Bridgehub.baseToken(chainId)`. ```ts -const base = await client.baseToken(324n /* example chain ID */); +{{#include ../../../snippets/viem/reference/client.test.ts:base}} ``` ### `getL2Wallet() β†’ viem.WalletClient` @@ -125,7 +96,7 @@ const base = await client.baseToken(324n /* example chain ID */); Return or lazily derive an L2 wallet from the same `account` as the L1 wallet. ```ts -const w = client.getL2Wallet(); // ensures L2 writes are possible +{{#include ../../../snippets/viem/reference/client.test.ts:l2-wallet}} ``` ## Types @@ -133,15 +104,7 @@ const w = client.getL2Wallet(); // ensures L2 writes are possible ### `ResolvedAddresses` ```ts -type ResolvedAddresses = { - bridgehub: Address; - l1AssetRouter: Address; - l1Nullifier: Address; - l1NativeTokenVault: Address; - l2AssetRouter: Address; - l2NativeTokenVault: Address; - l2BaseTokenSystem: Address; -}; +{{#include ../../../snippets/viem/reference/client.test.ts:resolved-type}} ``` ## Notes & Pitfalls diff --git a/docs/src/sdk-reference/viem/contracts.md b/docs/src/sdk-reference/viem/contracts.md index ea50767..66d470d 100644 --- a/docs/src/sdk-reference/viem/contracts.md +++ b/docs/src/sdk-reference/viem/contracts.md @@ -15,19 +15,9 @@ Resolved addresses and connected core contracts for the Viem adapter. ## Import ```ts -import { createPublicClient, createWalletClient, http } from 'viem'; -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; - -const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); -const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); -const l1Wallet = createWalletClient({ - account: /* your L1 Account */, - transport: http(process.env.ETH_RPC!), -}); - -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); -// sdk.contracts β†’ ContractsResource +{{#include ../../../snippets/viem/reference/contracts.test.ts:imports}} + +{{#include ../../../snippets/viem/reference/contracts.test.ts:init-sdk}} ``` ## Quick Start @@ -35,10 +25,7 @@ const sdk = createViemSdk(client); Resolve addresses and contract handles: ```ts -const addresses = await sdk.contracts.addresses(); -const { l1NativeTokenVault, l2AssetRouter } = await sdk.contracts.instances(); - -const ntv = await sdk.contracts.l1NativeTokenVault(); +{{#include ../../../snippets/viem/reference/contracts.test.ts:ntv}} ``` ## Method Reference @@ -48,18 +35,7 @@ const ntv = await sdk.contracts.l1NativeTokenVault(); Resolve core addresses (Bridgehub, routers, vaults, base-token system). ```ts -const a = await sdk.contracts.addresses(); -/* -{ - bridgehub, - l1AssetRouter, - l1Nullifier, - l1NativeTokenVault, - l2AssetRouter, - l2NativeTokenVault, - l2BaseTokenSystem -} -*/ +{{#include ../../../snippets/viem/reference/contracts.test.ts:addresses}} ``` ### `instances() β†’ Promise<{ ...contracts }>` @@ -67,8 +43,7 @@ const a = await sdk.contracts.addresses(); Return **typed** Viem contracts for all core components (each exposes `.read` / `.write` / `.simulate`). ```ts -const c = await sdk.contracts.instances(); -const bridgehub = c.bridgehub; +{{#include ../../../snippets/viem/reference/contracts.test.ts:instances}} ``` ### One-off Contract Getters @@ -84,7 +59,7 @@ const bridgehub = c.bridgehub; | `l2BaseTokenSystem()` | `Promise` | Connected L2 Base Token System. | ```ts -const router = await sdk.contracts.l2AssetRouter(); +{{#include ../../../snippets/viem/reference/contracts.test.ts:router}} ``` ## Notes & Pitfalls diff --git a/docs/src/sdk-reference/viem/deposits.md b/docs/src/sdk-reference/viem/deposits.md index 62640fb..00d4679 100644 --- a/docs/src/sdk-reference/viem/deposits.md +++ b/docs/src/sdk-reference/viem/deposits.md @@ -15,31 +15,9 @@ L1 β†’ L2 deposits for ETH and ERC-20 tokens with quote, prepare, create, status ## 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 '@matterlabs/zksync-js/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), -}); - -// Initialize -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); -// sdk.deposits β†’ DepositsResource +{{#include ../../../snippets/viem/reference/deposits.test.ts:imports}} + +{{#include ../../../snippets/viem/reference/deposits.test.ts:init-sdk}} ``` --- @@ -49,13 +27,7 @@ const sdk = createViemSdk(client); 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: account.address, -}); - -const l2Receipt = await sdk.deposits.wait(handle, { for: 'l2' }); // null only if no L1 hash +{{#include ../../../snippets/viem/reference/deposits.test.ts:create-deposit}} ``` > [!TIP] @@ -96,32 +68,7 @@ Estimate the deposit operation (route, approvals, gas hints). Does **not** send **Returns:** `DepositQuote` ```ts -const q = await sdk.deposits.quote({ - token: ETH_L1, - amount: parseEther('0.25'), - to: account.address, -}); -/* -{ - route: "eth-base" | "eth-nonbase" | "erc20-base" | "erc20-nonbase", - summary: { - route, - approvalsNeeded: [{ token, spender, amount }], - amounts: { - transfer: { token, amount } - }, - fees: { - token, - maxTotal, - mintValue, - l1: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, maxTotal }, - l2: { total, baseCost, operatorTip, gasLimit, maxFeePerGas, maxPriorityFeePerGas, gasPerPubdata } - }, - baseCost, - mintValue - } -} -*/ +{{#include ../../../snippets/viem/reference/deposits.test.ts:quote-deposit}} ``` > [!TIP] @@ -138,17 +85,7 @@ Build a 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 } - ] -} -*/ +{{#include ../../../snippets/viem/reference/deposits.test.ts:plan-deposit}} ``` ### `tryPrepare(p) β†’ Promise<{ ok: true; value: DepositPlan } | { ok: false; error }>` @@ -163,15 +100,7 @@ Returns a handle with the 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 -} -*/ +{{#include ../../../snippets/viem/reference/deposits.test.ts:handle}} ``` > [!WARNING] @@ -196,8 +125,7 @@ Accepts either a `DepositHandle` or a raw L1 tx hash. | `L2_FAILED` | L2 receipt found with `status !== 1` | ```ts -const s = await sdk.deposits.status(handle); -// { phase, l1TxHash, l2TxHash? } +{{#include ../../../snippets/viem/reference/deposits.test.ts:status}} ``` ### `wait(handleOrHash, { for: 'l1' | 'l2' }) β†’ Promise` @@ -208,8 +136,7 @@ Block until a checkpoint is reached. * `{ 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' }); +{{#include ../../../snippets/viem/reference/deposits.test.ts:wait}} ``` ### `tryWait(handleOrHash, opts) β†’ Result` @@ -223,103 +150,46 @@ Result-style `wait`. ### ETH Deposit (Typical) ```ts -const handle = await sdk.deposits.create({ - token: ETH_ADDRESS, - amount: parseEther('0.1'), - to: account.address, -}); - -await sdk.deposits.wait(handle, { for: 'l2' }); +{{#include ../../../snippets/viem/reference/deposits.test.ts:create-eth-deposit}} ``` ### ERC-20 Deposit (with Automatic Approvals) ```ts -const handle = await sdk.deposits.create({ - token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC example - amount: 1_000_000n, // 1 USDC (6 dp) - to: account.address, -}); - -const l1Receipt = await sdk.deposits.wait(handle, { for: 'l1' }); +{{#include ../../../snippets/viem/reference/deposits.test.ts:token-address}} +{{#include ../../../snippets/viem/reference/deposits.test.ts:create-token-deposit}} ``` ## Types (Overview) +### Deposit Params + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:params-type}} +``` + +### Deposit Quote + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:quote-type}} +``` + +### Deposit Plan + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:plan-type}} +``` + +### Deposit Waitable + +```ts +{{#include ../../../snippets/ethers/reference/deposits.test.ts:wait-type}} +``` + +### Deposit Status + ```ts -export interface DepositParams { - token: Address; // 0x…00 for ETH - amount: bigint; // wei - to?: Address; // L2 recipient - refundRecipient?: Address; - l2GasLimit?: bigint; - gasPerPubdata?: bigint; - operatorTip?: bigint; - l1TxOverrides?: Eip1559GasOverrides; -} - -export interface Eip1559GasOverrides { - gasLimit?: bigint; - maxFeePerGas?: bigint; - maxPriorityFeePerGas?: bigint; -} - -export interface DepositQuote { - route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; - summary: { - route: 'eth-base' | 'eth-nonbase' | 'erc20-base' | 'erc20-nonbase'; - approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; - amounts: { - transfer: { - token: Address; - amount: bigint; - }; - }; - fees: { - token: Address; - maxTotal: bigint; - mintValue: bigint; - l1: { - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas: bigint; - maxTotal: bigint; - }; - l2: { - total: bigint; - baseCost: bigint; - operatorTip: bigint; - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas: bigint; - gasPerPubdata: bigint; - }; - }; - baseCost: bigint; - mintValue: bigint; - }; -} - -export interface DepositPlan { - route: DepositQuote['route']; - summary: DepositQuote; - steps: Array<{ key: string; kind: string; tx: TTx }>; -} - -export interface DepositHandle { - kind: 'deposit'; - l1TxHash: Hex; - stepHashes: Record; - plan: DepositPlan; -} - -export 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 }; +{{#include ../../../snippets/ethers/reference/deposits.test.ts:status-type}} ``` > [!TIP] diff --git a/docs/src/sdk-reference/viem/finalization-services.md b/docs/src/sdk-reference/viem/finalization-services.md index 7a34678..11d3dc4 100644 --- a/docs/src/sdk-reference/viem/finalization-services.md +++ b/docs/src/sdk-reference/viem/finalization-services.md @@ -17,55 +17,15 @@ These utilities fetch the required L2β†’L1 proof data, check readiness, and subm ## Import & Setup ```ts -import { createPublicClient, createWalletClient, http, type Address } from 'viem'; -import { createViemClient, createViemSdk, createFinalizationServices } from '@matterlabs/zksync-js/viem'; +{{#include ../../../snippets/viem/reference/finalization-service.test.ts:imports}} -const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!) }); -const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); - -const l1Wallet = createWalletClient({ - account: /* your L1 Account */, - transport: http(process.env.ETH_RPC!), -}); - -const client = createViemClient({ l1, l2, l1Wallet }); -// optional: const sdk = createViemSdk(client); - -const svc = createFinalizationServices(client); +{{#include ../../../snippets/viem/reference/finalization-service.test.ts:init-sdk}} ``` ## Minimal Usage Example ```ts -import type { Hex } from 'viem'; - -const l2TxHash: Hex = '0x...'; - -// 1) Build finalize params + discover the L1 Nullifier to call -const { params, nullifier } = await svc.fetchFinalizeDepositParams(l2TxHash); - -// 2) (Optional) Check if already finalized -const already = await svc.isWithdrawalFinalized({ - chainIdL2: params.chainId, - l2BatchNumber: params.l2BatchNumber, - l2MessageIndex: params.l2MessageIndex, -}); -if (already) { - console.log('Already finalized on L1'); -} else { - // 3) Dry-run on L1 to confirm readiness (no gas spent) - const readiness = await svc.simulateFinalizeReadiness(params, nullifier); - - if (readiness.kind === 'READY') { - // 4) Submit finalize tx (signed by your L1 wallet) - const { hash, wait } = await svc.finalizeDeposit(params, nullifier); - console.log('L1 finalize tx:', hash); - const rcpt = await wait(); - console.log('Finalized in block:', rcpt.blockNumber); - } else { - console.warn('Not ready to finalize:', readiness); - } -} +{{#include ../../../snippets/viem/reference/finalization-service.test.ts:finalize-with-svc}} ``` > **Tip:** If you prefer the SDK to handle readiness checks automatically, call `sdk.withdrawals.finalize(l2TxHash)` instead. @@ -153,77 +113,9 @@ If you are also using `sdk.withdrawals.status(...)`, the phases align conceptual ## Types ```ts -// Finalize call input -export interface FinalizeDepositParams { - chainId: bigint; - l2BatchNumber: bigint; - l2MessageIndex: bigint; - l2Sender: Address; - l2TxNumberInBatch: number; - message: Hex; - merkleProof: Hex[]; -} - -// Key that identifies a withdrawal in the Nullifier mapping -export type WithdrawalKey = { - chainIdL2: bigint; - l2BatchNumber: bigint; - l2MessageIndex: bigint; -}; - -// Overall withdrawal state (used by higher-level status helpers) -type WithdrawalPhase = - | 'L2_PENDING' - | 'L2_INCLUDED' - | 'PENDING' - | 'READY_TO_FINALIZE' - | 'FINALIZING' - | 'FINALIZED' - | 'FINALIZE_FAILED' - | 'UNKNOWN'; - -export type WithdrawalStatus = { - phase: WithdrawalPhase; - l2TxHash: Hex; - l1FinalizeTxHash?: Hex; - key?: WithdrawalKey; -}; - -// Readiness result returned by simulateFinalizeReadiness(...) -export type FinalizeReadiness = - | { kind: 'READY' } - | { kind: 'FINALIZED' } - | { - kind: 'NOT_READY'; - // temporary, retry later - reason: 'paused' | 'batch-not-executed' | 'root-missing' | 'unknown'; - detail?: string; - } - | { - kind: 'UNFINALIZABLE'; - // permanent, won’t become ready - reason: 'message-invalid' | 'invalid-chain' | 'settlement-layer' | 'unsupported'; - detail?: string; - }; - -// Viem-bound service surface -export interface FinalizationServices { - fetchFinalizeDepositParams( - l2TxHash: Hex, - ): Promise<{ params: FinalizeDepositParams; nullifier: Address }>; - - isWithdrawalFinalized(key: WithdrawalKey): Promise; - - simulateFinalizeReadiness( - params: FinalizeDepositParams, - nullifier: Address, - ): Promise; - - finalizeDeposit( - params: FinalizeDepositParams, - nullifier: Address, - ): Promise<{ hash: string; wait: () => Promise }>; -} +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:status-type}} + +{{#include ../../../snippets/ethers/reference/finalization-service.test.ts:finalization-types}} ``` --- diff --git a/docs/src/sdk-reference/viem/sdk.md b/docs/src/sdk-reference/viem/sdk.md index 6e50bce..4fb244b 100644 --- a/docs/src/sdk-reference/viem/sdk.md +++ b/docs/src/sdk-reference/viem/sdk.md @@ -18,45 +18,21 @@ High-level SDK built on top of the **Viem adapter** β€” provides deposits, withd ## Import ```ts -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; +{{#include ../../../snippets/viem/reference/sdk.test.ts:sdk-import}} ``` ## Quick Start ```ts -import { createPublicClient, createWalletClient, http } from 'viem'; -import { createViemClient, createViemSdk, ETH_ADDRESS } from '@matterlabs/zksync-js/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 - 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.contracts.instances(); -const token = await sdk.tokens.resolve('0xYourToken'); -console.log(token.l2); +{{#include ../../../snippets/viem/reference/sdk.test.ts:sdk-import}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:viem-import}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:eth-import}} + +{{#include ../../../snippets/viem/reference/sdk.test.ts:init-sdk}} + +{{#include ../../../snippets/viem/reference/sdk.test.ts:erc-20-address}} + +{{#include ../../../snippets/viem/reference/sdk.test.ts:basic-sdk}} ``` > [!TIP] @@ -106,7 +82,7 @@ Utilities for resolved addresses and connected contracts. Token mapping lives in Resolve core addresses (Bridgehub, routers, vaults, base-token system). ```ts -const a = await sdk.contracts.addresses(); +{{#include ../../../snippets/viem/reference/sdk.test.ts:contract-addresses}} ``` ### `instances() β†’ Promise<{ ...contracts }>` @@ -114,8 +90,7 @@ const a = await sdk.contracts.addresses(); **Typed** Viem contracts for all core components (each exposes `.read` / `.write` / `.simulate`). ```ts -const c = await sdk.contracts.instances(); -const bridgehub = c.bridgehub; +{{#include ../../../snippets/viem/reference/sdk.test.ts:contract-instances}} ``` ### One-off Contract Getters @@ -131,7 +106,7 @@ const bridgehub = c.bridgehub; | `l2BaseTokenSystem()` | `Promise` | Connected L2 Base Token System. | ```ts -const nullifier = await sdk.contracts.l1Nullifier(); +{{#include ../../../snippets/viem/reference/sdk.test.ts:nullifier}} ``` --- diff --git a/docs/src/sdk-reference/viem/tokens.md b/docs/src/sdk-reference/viem/tokens.md index e52ba99..9b32c6b 100644 --- a/docs/src/sdk-reference/viem/tokens.md +++ b/docs/src/sdk-reference/viem/tokens.md @@ -14,20 +14,10 @@ Token identity, L1↔L2 mapping, bridge asset IDs, and chain token facts for ETH ## Import ```ts -import { http, createPublicClient, createWalletClient, parseEther } from 'viem'; -import { createViemClient, createViemSdk } from '@matterlabs/zksync-js/viem'; -import { mainnet } from 'viem/chains'; - -const l1 = createPublicClient({ transport: http(process.env.ETH_RPC!), chain: mainnet }); -const l2 = createPublicClient({ transport: http(process.env.ZKSYNC_RPC!) }); -const l1Wallet = createWalletClient({ - transport: http(process.env.ETH_RPC!), - account: process.env.PRIVATE_KEY! as `0x${string}`, - chain: mainnet, -}); - -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); +{{#include ../../../snippets/viem/reference/sdk.test.ts:viem-import}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:sdk-import}} + +{{#include ../../../snippets/viem/reference/sdk.test.ts:init-sdk}} // sdk.tokens β†’ TokensResource ``` @@ -36,34 +26,22 @@ const sdk = createViemSdk(client); Resolve a token by L1 address and fetch its L2 counterpart + bridge metadata: ```ts -const token = await sdk.tokens.resolve('0xYourTokenL1...'); -/* -{ - kind: 'eth' | 'base' | 'erc20', - l1: Address, - l2: Address, - assetId: Hex, - originChainId: bigint, - isChainEthBased: boolean, - baseTokenAssetId: Hex, - wethL1: Address, - wethL2: Address, -} -*/ +{{#include ../../../snippets/viem/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:resolve-token}} ``` Map addresses directly: ```ts -const l2Addr = await sdk.tokens.toL2Address('0xTokenL1...'); -const l1Addr = await sdk.tokens.toL1Address(l2Addr); +{{#include ../../../snippets/viem/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:map-token}} ``` Compute bridge identifiers: ```ts -const assetId = await sdk.tokens.assetIdOfL1('0xTokenL1...'); -const backL2 = await sdk.tokens.l2TokenFromAssetId(assetId); +{{#include ../../../snippets/viem/reference/sdk.test.ts:erc-20-address}} +{{#include ../../../snippets/viem/reference/sdk.test.ts:token-asset-ids}} ``` ## Method Reference @@ -101,4 +79,4 @@ Resolve a token reference into full metadata (kind, addresses, assetId, chain fa * **Caching:** `baseTokenAssetId`, `wethL1`, `wethL2`, and the origin chain id are memoized; repeated calls avoid extra RPC hits. * **ETH aliases:** Both `0xEeeee…` (ETH sentinel) and `FORMAL_ETH_ADDRESS` are normalized to canonical ETH. * **Base token alias:** `L2_BASE_TOKEN_ADDRESS` maps back to the L1 base token via `toL1Address`. -* **Error handling:** Methods throw typed errors via the adapters’ error handlers. Wrap with `try/catch` or rely on higher-level `try*` patterns. +* **Error handling:** Methods throw typed errors via the adapters’ error handlers. Wrap with `try/catch` or rely on higher-level `try*` patterns. diff --git a/docs/src/sdk-reference/viem/withdrawals.md b/docs/src/sdk-reference/viem/withdrawals.md index 60bf73d..f8240be 100644 --- a/docs/src/sdk-reference/viem/withdrawals.md +++ b/docs/src/sdk-reference/viem/withdrawals.md @@ -15,31 +15,9 @@ L2 β†’ L1 withdrawals for ETH and ERC-20 tokens with quote, prepare, create, sta ## 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 '@matterlabs/zksync-js/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), -}); - -// Initialize the SDK -const client = createViemClient({ l1, l2, l1Wallet }); -const sdk = createViemSdk(client); -// sdk.withdrawals β†’ WithdrawalsResource +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:imports}} + +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:init-sdk}} ``` ## Quick Start @@ -47,20 +25,9 @@ const sdk = createViemSdk(client); 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: account.address, // 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 }); +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:eth-import}} -// 3) Finalize on L1 (no-op if already finalized) -const { status, receipt: l1Receipt } = await sdk.withdrawals.finalize(handle.l2TxHash); +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:create-withdrawal}} ``` > [!INFO] @@ -96,25 +63,7 @@ Estimate the operation (route, approvals, gas hints). Does **not** send transact **Returns:** `WithdrawQuote` ```ts -const q = await sdk.withdrawals.quote({ token, amount, to }); -/* -{ - route: "base" | "erc20-nonbase", - summary: { - route, - approvalsNeeded: [{ token, spender, amount }], - amounts: { - transfer: { token, amount } - }, - fees: { - token, - maxTotal, - mintValue, - l2: { gasLimit, maxFeePerGas, maxPriorityFeePerGas, total } - } - } -} -*/ +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:quote}} ``` ### `tryQuote(p) β†’ Promise<{ ok: true; value: WithdrawQuote } | { ok: false; error }>` @@ -128,17 +77,7 @@ 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 }, - // … - ] -} -*/ +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:plan}} ``` ### `tryPrepare(p) β†’ Promise<{ ok: true; value: WithdrawPlan } | { ok: false; error }>` @@ -153,15 +92,7 @@ Returns a handle with the **L2 transaction hash**. **Returns:** `WithdrawHandle` ```ts -const handle = await sdk.withdrawals.create({ token, amount, to }); -/* -{ - kind: "withdrawal", - l2TxHash: Hex, - stepHashes: Record, - plan: WithdrawPlan -} -*/ +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:handle}} ``` > [!WARNING] @@ -186,8 +117,7 @@ Accepts a `WithdrawHandle` or raw **L2 tx hash**. | `FINALIZED` | Already finalized on L1 | ```ts -const s = await sdk.withdrawals.status(handle); -// { phase, l2TxHash, key? } +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:status}} ``` ### `wait(handleOrHash, { for: 'l2' | 'ready' | 'finalized', pollMs?, timeoutMs? })` @@ -199,9 +129,8 @@ Wait until the withdrawal reaches a specific phase. * `{ for: 'finalized' }` β†’ Resolves the **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 }); +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:receipt-1}} +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:receipt-2}} ``` > [!TIP] @@ -218,10 +147,7 @@ Send 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); -} +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:finalize}} ``` > [!INFO] @@ -235,85 +161,39 @@ Result-style `finalize`. ## End-to-End Example ```ts -const handle = await sdk.withdrawals.create({ token, amount, to }); +{{#include ../../../snippets/viem/reference/withdrawals.test.ts:min-happy-path}} +``` -// L2 inclusion -await sdk.withdrawals.wait(handle, { for: 'l2' }); +## Types (Overview) -// Option A: finalize immediately (throws if not ready) -await sdk.withdrawals.finalize(handle.l2TxHash); +### Withdraw Params -// Option B: wait for readiness, then finalize -await sdk.withdrawals.wait(handle, { for: 'ready' }); -await sdk.withdrawals.finalize(handle.l2TxHash); +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:params-type}} ``` -## Types (Overview) +### Withdraw Quote + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:quote-type}} +``` + +### Withdraw Plan + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:plan-type}} +``` + +### Withdraw Waitable + +```ts +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:wait-type}} +``` + +### Withdraw Status ```ts -export interface WithdrawParams { - token: Address; // L2 token (ETH sentinel supported) - amount: bigint; // wei - to?: Address; // L1 recipient - l2GasLimit?: bigint; - l2TxOverrides?: Eip1559GasOverrides; -} - -export interface Eip1559GasOverrides { - gasLimit?: bigint; - maxFeePerGas?: bigint; - maxPriorityFeePerGas?: bigint; -} - -export interface WithdrawQuote { - route: 'base' | 'erc20-nonbase'; - summary: { - route: 'base' | 'erc20-nonbase'; - approvalsNeeded: Array<{ token: Address; spender: Address; amount: bigint }>; - amounts: { - transfer: { - token: Address; - amount: bigint; - }; - }; - fees: { - token: Address; - maxTotal: bigint; - mintValue?: bigint; - l2?: { - gasLimit: bigint; - maxFeePerGas: bigint; - maxPriorityFeePerGas?: bigint; - total: bigint; - }; - }; - }; -} - -export interface WithdrawPlan { - route: WithdrawQuote['route']; - summary: WithdrawQuote; - steps: Array<{ key: string; kind: string; tx: TTx }>; -} - -export interface WithdrawHandle { - kind: 'withdrawal'; - l2TxHash: Hex; - stepHashes: Record; - plan: WithdrawPlan; -} - -export 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' }) -export type TransactionReceiptZKsyncOS = TransactionReceipt & { - l2ToL1Logs?: Array; -}; +{{#include ../../../snippets/ethers/reference/withdrawals.test.ts:status-type}} ``` --- diff --git a/llm/change-playbook.md b/llm/change-playbook.md index baf1b19..aa647a6 100644 --- a/llm/change-playbook.md +++ b/llm/change-playbook.md @@ -30,6 +30,7 @@ List all files you expect to modify. Consider: - Adapter implementations (`adapters/viem/`, `adapters/ethers/`) - Tests (`__tests__/`, `*.test.ts`) - Docs (`docs/src/`) +- Docs examples tests (`docs/snippets`) ### 3. Implement with Minimal Diff @@ -38,6 +39,8 @@ List all files you expect to modify. Consider: - Don't rename unless required - Don't "improve" unrelated code - Follow existing patterns exactly +- Don't add hardcoded code examples directly in any markdown docs. + Use imported code snippets from tests in `docs/snippets` in markdown docs files ### 4. Run Required Scripts @@ -57,18 +60,20 @@ If your change affects public API or behavior: - [ ] Update `docs/src/SUMMARY.md` (if adding new pages) - [ ] Update SDK reference docs (`docs/src/sdk-reference/viem/`, `docs/src/sdk-reference/ethers/`) - [ ] Add/update quickstart guide if new resource (follow deposits/withdrawals structure) +- [ ] Update the relevant docs examples tests in `docs/snippets` - [ ] Update LLM docs (`llm/`) if applicable --- ## Minimal Diff Principle -| Do | Don't | -| -------------------------- | ------------------------------- | -| Change only required lines | Reformat entire file | -| Fix the specific bug | Refactor "while you're there" | -| Add the specific feature | Add "nice to have" improvements | -| Update affected tests | Rewrite unrelated tests | +| Do | Don't | +| ----------------------------- | ----------------------------------------------- | +| Change only required lines | Reformat entire file | +| Fix the specific bug | Refactor "while you're there" | +| Add the specific feature | Add "nice to have" improvements | +| Update affected tests | Rewrite unrelated tests | +| Update affected docs snippets | Add hardcoded code examples into markdown files | --- @@ -80,14 +85,14 @@ If your change affects public API or behavior: 2. Write a failing test (if possible) 3. Fix the bug 4. Verify tests pass -5. Update docs if behavior changed +5. Update docs markdown files (`docs/src`) and relevant snippets (`docs/snippets`) if behavior changed ### Add Method to Existing Resource 1. Add type to `core/types/flows/.ts` 2. Implement in both adapters 3. Add tests -4. Update SDK reference docs +4. Update SDK reference docs markdown files (`docs/src/sdk-reference`) and relevant snippets (`docs/snippets`) ### Add New Resource diff --git a/llm/testing-and-quality.md b/llm/testing-and-quality.md index 0c29746..c795f48 100644 --- a/llm/testing-and-quality.md +++ b/llm/testing-and-quality.md @@ -8,18 +8,19 @@ All commands are in `package.json`. Run with `bun run