Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions docs/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
144 changes: 144 additions & 0 deletions docs/snippets/core/errors.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

/** 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<ErrorEnvelope, Envelope> = 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
})

});
133 changes: 133 additions & 0 deletions docs/snippets/core/rpc.test.ts
Original file line number Diff line number Diff line change
@@ -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<Address>;
// Fetches the Bytecode Supplier contract address.
getBytecodeSupplierAddress(): Promise<Address>;
// Fetches a proof for an L2→L1 log emitted in the given transaction.
getL2ToL1LogProof(txHash: Hex, index: number): Promise<ProofNormalized>;
// Fetches the transaction receipt, including the `l2ToL1Logs` field.
getReceiptWithL2ToL1(txHash: Hex): Promise<ReceiptWithL2ToL1 | null>;
// Fetches block metadata for the given block number.
getBlockMetadataByNumber(blockNumber: number): Promise<BlockMetadata | null>;
// Fetches the genesis configuration returned by `zks_getGenesis`.
getGenesis(): Promise<GenesisInput>;
}
// 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<ZksRpc, ZksType> = true;
const _proofType: Exact<ProofNormalized, ProofN> = true;
const _receiptType: Exact<ReceiptWithL2ToL1, RWithLog> = true;
const _genesisType: Exact<GenesisInput, GenesisType> = true;
const _metadataType: Exact<BlockMetadata, MetadataType> = 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();
});

});
8 changes: 8 additions & 0 deletions docs/snippets/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Exact<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2)
? (<T>() => T extends B ? 1 : 2) extends
(<T>() => T extends A ? 1 : 2)
? true
: false
: false;
Loading