|
| 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 |
0 commit comments