KiiWORKS / Documentation
Developer Guide
SOURCE · docs/developer-guide.md · synced from the engineering repo
Developer Guide
Adding a New Platform Service
Every platform service follows the same pattern. Here’s how to add one.
1. Create the Crate
mkdir -p platform/my-service/src
2. Cargo.toml
[package]
name = "kiiworks-my-service"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "My new service description"
[dependencies]
kiiworks-core = { workspace = true }
axum = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
async-trait = { workspace = true }
[dev-dependencies]
tower = { workspace = true }
hyper = { workspace = true }
tokio = { workspace = true }
3. Models (src/models.rs)
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyItem {
pub id: String,
pub name: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateMyItemRequest {
pub name: String,
}
4. State (src/state.rs)
The state struct must derive Clone for Axum’s state extraction:
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::models::MyItem;
#[derive(Clone)]
pub struct MyServiceState {
pub items: Arc<RwLock<HashMap<String, MyItem>>>,
}
impl MyServiceState {
pub fn new() -> Self {
Self {
items: Arc::new(RwLock::new(HashMap::new())),
}
}
}
5. Routes (src/routes.rs)
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use chrono::Utc;
use crate::models::*;
use crate::state::MyServiceState;
pub fn my_service_routes() -> Router<MyServiceState> {
Router::new()
.route("/my-service/items", post(create_item).get(list_items))
.route("/my-service/items/{id}", get(get_item))
}
async fn create_item(
State(state): State<MyServiceState>,
Json(body): Json<CreateMyItemRequest>,
) -> impl IntoResponse {
let id = uuid::Uuid::new_v4().to_string();
let item = MyItem {
id: id.clone(),
name: body.name,
created_at: Utc::now(),
};
let mut items = state.items.write().await;
items.insert(id, item.clone());
(StatusCode::CREATED, Json(item)).into_response()
}
async fn list_items(State(state): State<MyServiceState>) -> impl IntoResponse {
let items = state.items.read().await;
let list: Vec<MyItem> = items.values().cloned().collect();
(StatusCode::OK, Json(list)).into_response()
}
async fn get_item(
State(state): State<MyServiceState>,
Path(id): Path<String>,
) -> impl IntoResponse {
let items = state.items.read().await;
match items.get(&id) {
Some(item) => (StatusCode::OK, Json(item.clone())).into_response(),
None => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "not found"})),
).into_response(),
}
}
6. Library Root (src/lib.rs)
pub mod models;
pub mod state;
pub mod routes;
pub use state::MyServiceState;
pub use routes::my_service_routes;
7. Register in Workspace
Add to root Cargo.toml:
[workspace]
members = [
# ...existing members...
"platform/my-service",
]
[workspace.dependencies]
# ...existing deps...
kiiworks-my-service = { path = "platform/my-service" }
8. Register in Node
Add to platform/node/Cargo.toml:
[dependencies]
kiiworks-my-service = { workspace = true }
Add to platform/node/src/services.rs:
// My Service
let my_state = kiiworks_my_service::MyServiceState::new();
registry.register(
"my-service",
Some(kiiworks_my_service::my_service_routes().with_state(my_state)),
);
The .with_state(state) call is critical — it converts Router<MyServiceState> to Router<()> so the registry can merge it with other services.
9. Test
cargo build -p kiiworks-my-service
cargo test -p kiiworks-my-service
cargo build -p kiiworks-node # Verify integration
Testing Patterns
Unit Tests (In-Module)
Most services include tests in their routes.rs:
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[tokio::test]
async fn create_and_get_item() {
let state = MyServiceState::new();
let app = my_service_routes().with_state(state);
// Create
let response = app.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/my-service/items")
.header("Content-Type", "application/json")
.body(Body::from(r#"{"name":"test"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
}
Integration Tests
Live integration tests against Kaspa Testnet-12:
# Requires kaspad running on Testnet-12
cargo run -p kiiworks-testnet-integration
These are a binary, not cargo test, because they require sequential execution and network access.
Common Gotchas
gen is a Reserved Keyword (Rust 2024)
The gen keyword is reserved in Rust edition 2024. When using rand:
// WRONG — won't compile
let x: u32 = rng.gen();
// CORRECT — use raw identifier
let x: u32 = rand::Rng::r#gen(&mut rng);
Clone Required for Axum State
Axum’s State extractor requires Clone. If you get:
the trait bound `MyState: Clone` is not satisfied
Add #[derive(Clone)] to your state struct.
Router vs Router<()>
Services export Router<ServiceState>. The node needs Router<()>. The conversion:
// This returns Router<MyServiceState>
let routes = my_service_routes();
// .with_state() converts to Router<()>
let routes_for_registry = routes.with_state(state);
UTXO Contention
When running multiple blockchain tests sequentially, UTXOs from previous transactions may not be confirmed yet. Add std::thread::sleep(Duration::from_secs(2)) between transaction-heavy test phases.
NetworkId Doesn’t Impl Copy
NetworkId has a Custom(String) variant, so it can’t implement Copy. Use .clone():
let network = config.network.clone(); // Not *config.network
blob-storage Catch-All Routes
The {*key} catch-all route in blob-storage conflicts with {*key}/metadata. Use separate path prefixes to avoid ambiguity.
cargo clippy —fix and Test Imports
cargo clippy --fix can remove imports that are only used in #[cfg(test)] modules. Always check test modules after running auto-fix.
Project Conventions
File Structure
Every platform service follows:
platform/{name}/
├── Cargo.toml
└── src/
├── lib.rs # Re-exports
├── models.rs # Serde structs
├── state.rs # Clone state with Arc<RwLock<...>>
└── routes.rs # Axum handlers
Some services have additional files:
detector.rs(sentinel) — anomaly detection algorithmsengine.rs(workflow) — state machine logicmatcher.rs(circular) — geospatial matchinggraph.rs(lineage) — DAG algorithms
Naming
- Crate names:
kiiworks-{name}(kebab-case) - Module imports:
kiiworks_{name}(snake_case, Rust convention) - Service registration:
"{name}"matches the platform directory name - Route prefix:
/api/{service-name}/{service-name}/...(nested under api + service mount)
Error Responses
All services use consistent error responses:
{"error": "descriptive message"}
With appropriate HTTP status codes:
201 Created— resource created200 OK— success204 No Content— deleted400 Bad Request— invalid input404 Not Found— resource doesn’t exist409 Conflict— duplicate or state conflict429 Too Many Requests— rate limited
Logging
Use tracing macros with structured fields:
use tracing::info;
info!(item_id = %id, "created item");
info!(count = items.len(), "listed items");
Never log secrets, private keys, or mnemonics.
CLI Development
The CLI lives at tools/cli/ and uses clap for argument parsing:
tools/cli/src/
├── main.rs # Entry point, command dispatch
└── commands/
├── wallet.rs # Wallet commands
├── did.rs # DID commands
├── anchor.rs # Anchoring commands
├── vc.rs # Verifiable Credential commands
├── trusttag.rs # Trust tag commands
├── node.rs # Node start command
└── config.rs # Configuration commands
To add a new CLI command:
- Create
tools/cli/src/commands/my_command.rs - Add the subcommand to the
clapenum inmain.rs - Implement the handler using library crates directly (no HTTP)
Build & Release
Development Build
cargo build --workspace
Release Build
cargo build --release -p kiiworks-cli
# Binary: target/release/kiiworks
Release profile (from Cargo.toml):
- LTO enabled (link-time optimisation)
- Single codegen unit (slower build, faster binary)
- Symbols stripped
- Panic = abort (smaller binary)
Lint
cargo clippy --workspace # Must be 0 warnings
Format
cargo fmt --all # Uses rustfmt.toml settings