Skip to content

Commit 8fca616

Browse files
committed
Merge branch 'main' into refactor/paymaster-v2
2 parents 5525164 + affddaa commit 8fca616

File tree

38 files changed

+3429
-55
lines changed

38 files changed

+3429
-55
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
---
2+
name: implement-rpc-api
3+
description: How to implement a new JSON-RPC API in this codebase — defining the API trait, types, error enum, and server handler.
4+
---
5+
6+
# Implement a New JSON-RPC API
7+
8+
This skill describes how to add a new JSON-RPC API namespace to Katana. There are five steps:
9+
10+
1. Define the API trait (`katana-rpc-api`)
11+
2. Define custom types, if needed (`katana-rpc-types`)
12+
3. Define a dedicated error enum (`katana-rpc-api`)
13+
4. Implement the server handler (`katana-rpc-server`)
14+
5. Register the API in the node implementation(s) (`katana-sequencer-node`/`katana-full-node` for sequencer/full node respectively)
15+
16+
The crates involved:
17+
18+
| Crate | Path | Purpose |
19+
|---|---|---|
20+
| `katana-rpc-api` | `crates/rpc/rpc-api/` | API trait definitions and error types |
21+
| `katana-rpc-types` | `crates/rpc/rpc-types/` | RPC request/response types |
22+
| `katana-rpc-server` | `crates/rpc/rpc-server/` | Server-side implementations |
23+
| `katana-sequencer-node` | `crates/node/sequencer/` | Sequencer node — wires RPC modules into the server |
24+
| `katana-full-node` | `crates/node/full/` | Full node — wires RPC modules into the server |
25+
| `katana-node-config` | `crates/node/config/` | Node configuration including `RpcModuleKind` |
26+
27+
Throughout this guide, `<name>` is the API namespace (e.g., `dev`, `tee`, `starknet`).
28+
29+
---
30+
31+
## Step 1: Define the API Trait
32+
33+
Create a new module in `crates/rpc/rpc-api/src/<name>.rs` and define the trait using the `jsonrpsee` proc macro.
34+
35+
### Naming conventions
36+
37+
- **Trait name**: `<Name>Api` — PascalCase of the namespace with `Api` suffix (e.g., `DevApi`, `TeeApi`).
38+
- **Namespace**: The `namespace` attribute in the `#[rpc(...)]` macro must match the JSON-RPC namespace exactly (e.g., `"dev"` produces methods like `dev_generateBlock`).
39+
- **Method names**: Use the `#[method(name = "...")]` attribute with camelCase (e.g., `"generateBlock"`). The Rust function name uses snake_case.
40+
41+
### Template
42+
43+
```rust
44+
use jsonrpsee::core::RpcResult;
45+
use jsonrpsee::proc_macros::rpc;
46+
47+
#[cfg_attr(not(feature = "client"), rpc(server, namespace = "<name>"))]
48+
#[cfg_attr(feature = "client", rpc(client, server, namespace = "<name>"))]
49+
pub trait <Name>Api {
50+
/// Brief description of what this method does.
51+
#[method(name = "methodName")]
52+
async fn method_name(&self, param: ParamType) -> RpcResult<ResponseType>;
53+
}
54+
```
55+
56+
### Key rules
57+
58+
- All methods must be `async`.
59+
- Return type is always `RpcResult<T>` (alias for `Result<T, jsonrpsee::types::ErrorObjectOwned>`).
60+
- The `#[cfg_attr]` pattern enables client code generation only when the `client` feature is active, keeping the server build lighter.
61+
- If a method can have a default implementation (e.g., returning a constant), implement it directly in the trait body. See `StarknetApi::spec_version` for an example.
62+
63+
### Register the module
64+
65+
Add the new module to `crates/rpc/rpc-api/src/lib.rs`:
66+
67+
```rust
68+
pub mod <name>;
69+
```
70+
71+
If the API is feature-gated:
72+
73+
```rust
74+
#[cfg(feature = "<feature>")]
75+
pub mod <name>;
76+
```
77+
78+
### Reference examples
79+
80+
- **Simple API**: `crates/rpc/rpc-api/src/dev.rs``DevApi` with straightforward methods.
81+
- **Feature-gated API**: `crates/rpc/rpc-api/src/tee.rs``TeeApi` behind the `tee` feature.
82+
- **Split API (read/write/trace)**: `crates/rpc/rpc-api/src/starknet.rs` — Multiple traits sharing the same namespace.
83+
84+
---
85+
86+
## Step 2: Define Custom Types (if needed)
87+
88+
If the API uses request/response types that don't already exist in `katana-primitives` or `katana-rpc-types`, define them in `crates/rpc/rpc-types/src/`.
89+
90+
### Conventions
91+
92+
- All types must derive `Debug`, `Clone`, `Serialize`, `Deserialize`.
93+
- Use `#[serde(rename_all = "camelCase")]` for field names that should be camelCase in JSON.
94+
- Use `#[serde(tag = "type")]` for enum variants that should be discriminated by a `type` field.
95+
- Use `#[serde(flatten)]` to inline nested structs.
96+
- Hex-encoded numeric fields use custom serializers from `serde_utils` (e.g., `serialize_as_hex`, `deserialize_u128`).
97+
- Types that map to internal primitives (`katana-primitives`) should implement `From` conversions.
98+
99+
### Template
100+
101+
```rust
102+
use serde::{Deserialize, Serialize};
103+
104+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105+
#[serde(rename_all = "camelCase")]
106+
pub struct <Name>Response {
107+
pub field_one: String,
108+
pub field_two: u64,
109+
}
110+
```
111+
112+
---
113+
114+
## Step 3: Define a Dedicated Error Enum
115+
116+
Every API namespace must have its own error enum in `crates/rpc/rpc-api/src/error/`. Create `crates/rpc/rpc-api/src/error/<name>.rs`.
117+
118+
### Template
119+
120+
```rust
121+
use jsonrpsee::types::ErrorObjectOwned;
122+
123+
#[derive(thiserror::Error, Clone, Debug)]
124+
pub enum <Name>ApiError {
125+
#[error("Description of error A")]
126+
ErrorA,
127+
128+
#[error("Description of error B: {0}")]
129+
ErrorB(String),
130+
}
131+
132+
impl From<<Name>ApiError> for ErrorObjectOwned {
133+
fn from(err: <Name>ApiError) -> Self {
134+
let code = match &err {
135+
<Name>ApiError::ErrorA => 1,
136+
<Name>ApiError::ErrorB(_) => 2,
137+
};
138+
ErrorObjectOwned::owned(code, err.to_string(), None::<()>)
139+
}
140+
}
141+
```
142+
143+
### Key rules
144+
145+
- Use `thiserror::Error` for the enum.
146+
- Each variant maps to a unique `i32` error code.
147+
- Implement `From<...> for ErrorObjectOwned` so errors convert to JSON-RPC errors automatically.
148+
- For errors that carry structured data, pass `Some(data)` instead of `None::<()>` in `ErrorObjectOwned::owned(...)`. Define the data struct with `Serialize + Deserialize`.
149+
- Pick error codes that don't conflict with existing APIs. Check `crates/rpc/rpc-api/src/error/` for codes already in use.
150+
151+
### Register the error module
152+
153+
Add to `crates/rpc/rpc-api/src/error/mod.rs`:
154+
155+
```rust
156+
pub mod <name>;
157+
```
158+
159+
### Reference examples
160+
161+
- **Simple (code-as-discriminant)**: `error/katana.rs` — integer error codes via `#[repr(i32)]` enum discriminants.
162+
- **With structured data**: `error/dev.rs``UnexpectedErrorData` passed as error data.
163+
- **Feature-gated**: `error/tee.rs` — error codes starting at 100 to avoid conflicts.
164+
165+
---
166+
167+
## Step 4: Implement the Server Handler
168+
169+
Create a new module in `crates/rpc/rpc-server/src/<name>.rs` (or `crates/rpc/rpc-server/src/<name>/mod.rs` if the implementation is large enough to split into submodules).
170+
171+
### Structure
172+
173+
1. **Handler struct** — holds the state/dependencies the API needs.
174+
2. **Internal methods** — business logic returning `Result<T, <Name>ApiError>`.
175+
3. **Trait impl** — implements the `<Name>ApiServer` trait (generated by the proc macro), delegating to internal methods.
176+
177+
### Template
178+
179+
```rust
180+
use std::sync::Arc;
181+
182+
use jsonrpsee::core::{async_trait, RpcResult};
183+
use katana_rpc_api::<name>::<Name>ApiServer;
184+
use katana_rpc_api::error::<name>::<Name>ApiError;
185+
186+
#[allow(missing_debug_implementations)]
187+
pub struct <Name>Api {
188+
// Dependencies: storage providers, backend, etc.
189+
// Wrap shared state in Arc for cheap cloning.
190+
}
191+
192+
impl <Name>Api {
193+
pub fn new(/* deps */) -> Self {
194+
Self { /* ... */ }
195+
}
196+
197+
// Internal methods with concrete error types.
198+
fn some_internal_method(&self) -> Result<(), <Name>ApiError> {
199+
// ...
200+
Ok(())
201+
}
202+
}
203+
204+
#[async_trait]
205+
impl <Name>ApiServer for <Name>Api {
206+
async fn method_name(&self, param: ParamType) -> RpcResult<ResponseType> {
207+
// Delegate to internal method; the ? operator converts
208+
// <Name>ApiError -> ErrorObjectOwned automatically.
209+
Ok(self.some_internal_method()?)
210+
}
211+
}
212+
```
213+
214+
### Key patterns
215+
216+
- **Generic over storage**: If the handler needs storage access, make it generic over `ProviderFactory` (see `DevApi<PF>` and `TeeApi<PF>`).
217+
- **Arc for shared state**: Wrap inner state in `Arc` if the handler needs to be cloned (required when registering with jsonrpsee).
218+
- **Blocking tasks**: For I/O-heavy or CPU-heavy work, use `on_io_blocking_task` or `on_cpu_blocking_task` patterns (see `StarknetApi`). For simpler APIs this isn't needed.
219+
- **Error conversion**: The `?` operator chains `From<ApiError> for ErrorObjectOwned` so that trait methods can use `Ok(self.internal_method()?)`.
220+
221+
### Register the module
222+
223+
Add to `crates/rpc/rpc-server/src/lib.rs`:
224+
225+
```rust
226+
pub mod <name>;
227+
```
228+
229+
If the API is feature-gated:
230+
231+
```rust
232+
#[cfg(feature = "<feature>")]
233+
pub mod <name>;
234+
```
235+
236+
### Reference examples
237+
238+
- **Simple handler**: `crates/rpc/rpc-server/src/dev.rs``DevApi` with direct method calls.
239+
- **Generic over storage**: `crates/rpc/rpc-server/src/tee.rs``TeeApi<PF>` parameterized by provider factory.
240+
- **Complex handler with submodules**: `crates/rpc/rpc-server/src/starknet/` — split into `read.rs`, `write.rs`, `trace.rs`.
241+
242+
---
243+
244+
## Step 5: Register the API in the Sequencer Node (same for Full Node)
245+
246+
The final step is wiring the new API into the node so it actually gets served. Registration happens in `Node::build_with_provider` in `crates/node/sequencer/src/lib.rs`. There are also other node implementations (e.g., `crates/node/full/src/lib.rs`) that may need the same registration if applicable.
247+
248+
### 5a. Add a variant to `RpcModuleKind`
249+
250+
If the API should be toggleable at runtime (most APIs should be), add a variant to the `RpcModuleKind` enum in `crates/node/config/src/rpc.rs`:
251+
252+
```rust
253+
#[derive(Debug, Clone, PartialEq, Eq)]
254+
pub enum RpcModuleKind {
255+
Starknet,
256+
Dev,
257+
Katana,
258+
// Add your new variant:
259+
<Name>,
260+
}
261+
```
262+
263+
For feature-gated APIs, annotate the variant:
264+
265+
```rust
266+
#[cfg(feature = "<feature>")]
267+
<Name>,
268+
```
269+
270+
### 5b. Register in `Node::build_with_provider`
271+
272+
In `crates/node/sequencer/src/lib.rs`, add the imports and registration block. The registration goes in the `build_with_provider` method, in the section where other RPC modules are merged into `rpc_modules` (around lines 309–358).
273+
274+
**Add imports** at the top of the file:
275+
276+
```rust
277+
use katana_rpc_api::<name>::<Name>ApiServer;
278+
use katana_rpc_server::<name>::<Name>Api;
279+
```
280+
281+
For feature-gated APIs, wrap the imports:
282+
283+
```rust
284+
#[cfg(feature = "<feature>")]
285+
use katana_rpc_api::<name>::<Name>ApiServer;
286+
#[cfg(feature = "<feature>")]
287+
use katana_rpc_server::<name>::<Name>Api;
288+
```
289+
290+
**Add the registration block** alongside the existing API registrations:
291+
292+
```rust
293+
// --- Always-on API (like Dev)
294+
if config.rpc.apis.contains(&RpcModuleKind::<Name>) {
295+
let api = <Name>Api::new(/* deps from the build context: backend, pool, provider, etc. */);
296+
rpc_modules.merge(<Name>ApiServer::into_rpc(api))?;
297+
}
298+
```
299+
300+
For feature-gated APIs (like TEE):
301+
302+
```rust
303+
#[cfg(feature = "<feature>")]
304+
if config.rpc.apis.contains(&RpcModuleKind::<Name>) {
305+
let api = <Name>Api::new(/* deps */);
306+
rpc_modules.merge(<Name>ApiServer::into_rpc(api))?;
307+
}
308+
```
309+
310+
### Where to place it
311+
312+
The registration block goes **after** the existing API registrations and **before** the `RpcServer::new()` builder call. Follow the existing ordering in `build_with_provider`:
313+
314+
1. Paymaster/Cartridge APIs (feature-gated)
315+
2. StarknetApi (read, write, trace)
316+
3. KatanaApi
317+
4. DevApi
318+
5. TeeApi (feature-gated)
319+
6. **Your new API goes here**
320+
7. `RpcServer::new().module(rpc_modules)?` — builds the server
321+
322+
### Available dependencies
323+
324+
Inside `build_with_provider`, these objects are available to pass to your handler constructor:
325+
326+
| Variable | Type | Description |
327+
|---|---|---|
328+
| `backend` | `Arc<Backend<P>>` | Node backend (chain spec, executor, storage, gas oracle) |
329+
| `block_producer` | `BlockProducer<P>` | Block production control |
330+
| `pool` | `TxPool` | Transaction mempool |
331+
| `provider` | `P` (impl `ProviderFactory`) | Storage provider factory |
332+
| `task_spawner` | `TaskSpawner` | Async task spawner for blocking work |
333+
| `gas_oracle` | `GasPriceOracle` | Gas price oracle |
334+
| `config` | `Config` | Full node configuration |
335+
336+
### 5c. Don't forget other node implementations
337+
338+
If the API should also be available in the full node (not just the sequencer), apply the same registration in `crates/node/full/src/lib.rs`. The pattern is identical.
339+
340+
### 5d. Add `Cargo.toml` dependencies
341+
342+
Add the `katana-rpc-api` and `katana-rpc-server` crates as dependencies of the node crate (`crates/node/sequencer/Cargo.toml`) if they aren't already listed. For feature-gated APIs, gate the dependencies under the appropriate feature.
343+
344+
### Reference examples
345+
346+
Look at how existing APIs are registered in `crates/node/sequencer/src/lib.rs`:
347+
348+
- **Always-on, simple**: DevApi (lines 324–327) — conditional on `RpcModuleKind::Dev`.
349+
- **Always-on, multi-trait**: StarknetApi (lines 309–322) — registers read, write, trace, and katana traits from the same handler.
350+
- **Feature-gated**: TeeApi (lines 330–358) — guarded by `#[cfg(feature = "tee")]` and `RpcModuleKind::Tee`, with provider initialization logic.
351+
352+
---
353+
354+
## Checklist
355+
356+
- [ ] API trait defined in `crates/rpc/rpc-api/src/<name>.rs`
357+
- [ ] Module added to `crates/rpc/rpc-api/src/lib.rs`
358+
- [ ] Error enum defined in `crates/rpc/rpc-api/src/error/<name>.rs`
359+
- [ ] Error module added to `crates/rpc/rpc-api/src/error/mod.rs`
360+
- [ ] Custom types defined in `crates/rpc/rpc-types/src/` (if needed)
361+
- [ ] Server handler implemented in `crates/rpc/rpc-server/src/<name>.rs`
362+
- [ ] Handler module added to `crates/rpc/rpc-server/src/lib.rs`
363+
- [ ] `RpcModuleKind` variant added in `crates/node/config/src/rpc.rs`
364+
- [ ] API registered in `Node::build_with_provider` (`crates/node/sequencer/src/lib.rs`)
365+
- [ ] API registered in full node if applicable (`crates/node/full/src/lib.rs`)
366+
- [ ] Dependencies added to the relevant `Cargo.toml` files

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ AGENT.md
77
# Build artifacts and caches
88
target/
99
build/
10+
!crates/contracts/build/
1011
monitoring/
1112

1213
# CI/CD files (except the Dockerfile being built)

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ crates/contracts/build/
4040
!crates/contracts/build/legacy/
4141

4242
**/.claude/settings.local.json
43+
44+
vendor
45+
katana-tee
46+
47+
# AMDSEV build artifacts
48+
misc/AMDSEV/ovmf/
49+
misc/AMDSEV/output/
50+
misc/AMDSEV/source-commit.ovmf

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- SKILLS_INDEX_START -->
2-
[Agent Skills Index]|root: ./agents|IMPORTANT: Prefer retrieval-led reasoning over pre-training for any tasks covered by skills.|skills|create-a-plan:{create-a-plan.md},create-pr:{create-pr.md}
2+
[Agent Skills Index]|root: ./agents|IMPORTANT: Prefer retrieval-led reasoning over pre-training for any tasks covered by skills.|skills|create-a-plan:{create-a-plan.md},create-pr:{create-pr.md},implement-rpc-api:{implement-rpc-api.md}
33
<!-- SKILLS_INDEX_END -->
44
# Repository Guidelines
55

0 commit comments

Comments
 (0)