|
| 1 | +# Architecture: Adapters and Core |
| 2 | + |
| 3 | +> **The boundary between `core/` and adapters is critical. Read this carefully.** |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Hard Rules |
| 8 | + |
| 9 | +> [!CAUTION] |
| 10 | +> These rules are **non-negotiable**. |
| 11 | +
|
| 12 | +### 1. `core/` Must Never Depend on Adapters |
| 13 | + |
| 14 | +```typescript |
| 15 | +// ❌ NEVER in core/ |
| 16 | +import { encodeFunctionData } from 'viem'; |
| 17 | +import { AbiCoder } from 'ethers'; |
| 18 | +import type { Address } from 'viem'; |
| 19 | +import type { Provider } from 'ethers'; |
| 20 | + |
| 21 | +// ✅ OK in core/ |
| 22 | +import type { Hex, Address } from './types/primitives'; |
| 23 | +import { ZKsyncError } from './errors'; |
| 24 | +``` |
| 25 | + |
| 26 | +### 2. Adapters Use Their Library's Encoders/Decoders |
| 27 | + |
| 28 | +```typescript |
| 29 | +// ❌ NEVER hand-roll encode/decode |
| 30 | +const encoded = '0x' + functionSelector + param1.slice(2) + param2.slice(2); |
| 31 | + |
| 32 | +// ✅ Viem adapter |
| 33 | +import { encodeFunctionData, decodeFunctionResult } from 'viem'; |
| 34 | +const encoded = encodeFunctionData({ abi, functionName, args }); |
| 35 | + |
| 36 | +// ✅ Ethers adapter |
| 37 | +import { Interface, AbiCoder } from 'ethers'; |
| 38 | +const iface = new Interface(abi); |
| 39 | +const encoded = iface.encodeFunctionData(functionName, args); |
| 40 | +``` |
| 41 | + |
| 42 | +### 3. Shared Logic Lives in `core/` |
| 43 | + |
| 44 | +If logic is duplicated across adapters, extract it to `core/`: |
| 45 | + |
| 46 | +```typescript |
| 47 | +// core/utils/gas.ts |
| 48 | +export function calculateL2GasLimit(baseCost: bigint, overhead: bigint): bigint { |
| 49 | + return baseCost + overhead; |
| 50 | +} |
| 51 | + |
| 52 | +// adapters/viem/resources/deposits.ts |
| 53 | +import { calculateL2GasLimit } from '../../../core/utils/gas'; |
| 54 | + |
| 55 | +// adapters/ethers/resources/deposits.ts |
| 56 | +import { calculateL2GasLimit } from '../../../core/utils/gas'; |
| 57 | +``` |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Adapter Responsibilities |
| 62 | + |
| 63 | +Adapters are **translation layers only**. They: |
| 64 | + |
| 65 | +| Do | Don't | |
| 66 | +| ------------------------------------- | -------------------------------------- | |
| 67 | +| Translate core types to library types | Contain business logic | |
| 68 | +| Call library methods (viem/ethers) | Duplicate logic across adapters | |
| 69 | +| Map library results to core types | Define new types (types live in core/) | |
| 70 | +| Handle library-specific errors | Hand-roll ABI encoding/decoding | |
| 71 | + |
| 72 | +### Adapter Checklist |
| 73 | + |
| 74 | +For any adapter code: |
| 75 | + |
| 76 | +- [ ] Shared, adapter-agnostic logic extracted to `core/` (business logic requiring adapter imports is fine here) |
| 77 | +- [ ] Uses library's native encoding/decoding |
| 78 | +- [ ] Returns core types |
| 79 | +- [ ] Accepts core types as input (or maps from them) |
| 80 | +- [ ] Error handling wraps library errors into `ZKsyncError` |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## Acceptable vs Unacceptable |
| 85 | + |
| 86 | +### Imports in `core/` |
| 87 | + |
| 88 | +```typescript |
| 89 | +// ✅ Acceptable |
| 90 | +import type { Hex, Address } from './types/primitives'; |
| 91 | +import { ZKsyncError } from './errors'; |
| 92 | +import { BRIDGEHUB_ABI } from './internal/abis'; |
| 93 | + |
| 94 | +// ❌ Unacceptable |
| 95 | +import { type Address } from 'viem'; |
| 96 | +import { ethers } from 'ethers'; |
| 97 | +import { encodeFunctionData } from 'viem'; |
| 98 | +``` |
| 99 | + |
| 100 | +### Types in `core/` |
| 101 | + |
| 102 | +```typescript |
| 103 | +// ✅ Core defines primitives and flow types |
| 104 | +// core/types/primitives.ts |
| 105 | +export type Address = `0x${string}`; |
| 106 | +export type Hex = `0x${string}`; |
| 107 | +export type Hash = `0x${string}`; |
| 108 | + |
| 109 | +// core/types/flows/deposit.ts |
| 110 | +export interface DepositRequest { |
| 111 | + token: Address; |
| 112 | + amount: bigint; |
| 113 | + to: Address; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +### Adapter Implementation |
| 118 | + |
| 119 | +```typescript |
| 120 | +// ✅ Adapter translates and calls library |
| 121 | +// adapters/viem/resources/deposits.ts |
| 122 | +import { encodeFunctionData } from 'viem'; |
| 123 | +import type { DepositRequest } from '../../../core/types/flows/deposit'; |
| 124 | + |
| 125 | +export function prepareDeposit(ctx: ViemContext, req: DepositRequest) { |
| 126 | + const data = encodeFunctionData({ |
| 127 | + abi: BRIDGEHUB_ABI, |
| 128 | + functionName: 'requestL2TransactionDirect', |
| 129 | + args: [req.to, req.amount, ...], |
| 130 | + }); |
| 131 | + // ... |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## When to Add to Core vs Adapter |
| 138 | + |
| 139 | +| Scenario | Location | |
| 140 | +| ----------------------------------- | ------------------------------- | |
| 141 | +| New type definition | `core/types/` | |
| 142 | +| New constant (address, magic value) | `core/constants.ts` | |
| 143 | +| Utility used by multiple adapters | `core/utils/` | |
| 144 | +| ABI definition | `core/internal/abis/` | |
| 145 | +| Library-specific call | Adapter | |
| 146 | +| Library-specific error handling | Adapter (wrap to `ZKsyncError`) | |
0 commit comments