# dig-rpc-types
Pure `serde`-derived JSON-RPC wire types for the DIG Network fullnode / validator / future wallet RPC surface. The single source of truth for every request and response shape exchanged between a DIG server and its clients.
- **No I/O** — no tokio, no reqwest, no Axum in the default build.
- **No business logic** — a method's wire shape is defined; dispatch lives elsewhere.
- **Stable error codes** — [`ErrorCode`](#errorcode) numeric values never change once assigned.
- **Additive enum evolution** — every enum is `#[non_exhaustive]`.
- **JSON-RPC 2.0 strict** — request/response envelopes match the spec exactly.
See [`docs/resources/SPEC.md`](docs/resources/SPEC.md) for the design doc.
---
## Table of contents
1. [Install](#install)
2. [At-a-glance](#at-a-glance)
3. [Quick reference](#quick-reference)
4. [Envelope types](#envelope-types)
5. [`ErrorCode`](#errorcode)
6. [Shared domain types](#shared-domain-types)
7. [Fullnode methods](#fullnode-methods)
8. [Validator methods](#validator-methods)
9. [Stability contract](#stability-contract)
10. [Feature flags](#feature-flags)
11. [License](#license)
---
## Install
```toml
[dependencies]
dig-rpc-types = "0.1"
```
Minimum Rust: 1.70. Zero optional features are required for the core wire types.
---
## At-a-glance
| `envelope` | ✓ | JSON-RPC 2.0 request / response / error / id / version |
| `errors` | ✓ | Stable numeric error codes (`ErrorCode`) |
| `types` | ✓ (partial) | Shared domain types (HashHex, PubkeyHex, SignatureHex, Amount, BlockSummary, ValidatorSummary, ValidatorStatus) |
| `fullnode` | — | 21 fullnode RPC method pairs |
| `validator` | — | 9 validator RPC method pairs |
Every method's request carries a `pub const METHOD: &'static str` for the wire name (`GetBlockchainStateRequest::METHOD == "get_blockchain_state"`).
---
## Quick reference
```rust,no_run
use dig_rpc_types::{
envelope::{JsonRpcRequest, JsonRpcResponse, JsonRpcResponseBody, RequestId, Version},
fullnode::{GetBlockchainStateRequest, GetBlockchainStateResponse},
};
// Build a request.
let req: JsonRpcRequest<()> = JsonRpcRequest {
jsonrpc: Version,
id: RequestId::Num(1),
method: GetBlockchainStateRequest::METHOD.to_string(),
params: None,
};
// Decode a typed response off the wire.
let raw = r#"{"jsonrpc":"2.0","id":1,"result":{
"height": 100, "tip_hash":"0x1111111111111111111111111111111111111111111111111111111111111111",
"synced":true,"sync_progress":1.0,"finalized_epoch": 3, "sealed_height": 95
}}"#;
let _resp: JsonRpcResponse<GetBlockchainStateResponse> = serde_json::from_str(raw).unwrap();
```
---
## Envelope types
Module: `dig_rpc_types::envelope`. All re-exported at the crate root.
### `JsonRpcRequest<P = serde_json::Value>`
```rust,ignore
pub struct JsonRpcRequest<P = serde_json::Value> {
pub jsonrpc: Version, // serialises to literal "2.0"
pub id: RequestId,
pub method: String,
pub params: Option<P>, // skip_serializing_if = "Option::is_none"
}
```
**Inputs to construct:** any `P` that implements `Serialize`. For typed requests, use the method-specific `{Method}Request` types under `fullnode::` / `validator::` as `P`. For generic proxies, leave `P = serde_json::Value`.
**Output on the wire:** `{"jsonrpc":"2.0","id":...,"method":"...","params":...}` — `params` is omitted entirely when `None`.
### `JsonRpcResponse<R = serde_json::Value>`
```rust,ignore
pub struct JsonRpcResponse<R = serde_json::Value> {
pub jsonrpc: Version,
pub id: RequestId,
#[serde(flatten)]
pub body: JsonRpcResponseBody<R>,
}
pub enum JsonRpcResponseBody<R> { // #[serde(untagged)]
Success { result: R },
Error { error: JsonRpcError },
}
```
**Output on the wire:** exactly one of `{"jsonrpc":"2.0","id":...,"result":...}` or `{"jsonrpc":"2.0","id":...,"error":{...}}` — never both, never neither. This is enforced by the untagged enum.
### `JsonRpcError`
```rust,ignore
pub struct JsonRpcError {
pub code: ErrorCode,
pub message: String,
pub data: Option<serde_json::Value>, // skip_serializing_if = "Option::is_none"
}
```
**Inputs:** `code` is any [`ErrorCode`](#errorcode) variant; `message` is a human-readable short description; `data` carries optional structured context.
### `RequestId`
```rust,ignore
pub enum RequestId { // #[serde(untagged)]
Num(u64),
Str(String),
Null, // spec-allowed for notifications
}
```
Echoed unchanged in the matching response.
### `Version`
```rust,ignore
pub struct Version; // ZST; serde impls hard-code "2.0"
```
Rejecting any string other than `"2.0"` at deserialize time is structural — no hand-coded check needed.
---
## `ErrorCode`
Module: `dig_rpc_types::errors`. Re-exported at the crate root.
`#[repr(i32)]`, `#[non_exhaustive]`, `Serialize_repr` / `Deserialize_repr` — serialises as a bare integer on the wire, matching JSON-RPC 2.0.
### Variants and wire values
| `ParseError` | `-32700` | JSON-RPC reserved | Invalid JSON received |
| `InvalidRequest` | `-32600` | JSON-RPC reserved | JSON valid but not a Request |
| `MethodNotFound` | `-32601` | JSON-RPC reserved | Unknown method |
| `InvalidParams` | `-32602` | JSON-RPC reserved | Params don't match schema |
| `InternalError` | `-32603` | JSON-RPC reserved | Server bug / panic caught |
| `NotSynced` | `10001` | DIG sync/auth | Server not yet synced; data may be stale |
| `PeerUnauthorized` | `10002` | DIG sync/auth | Client cert unresolved to a role |
| `PermissionDenied` | `10003` | DIG sync/auth | Role below method's `min_role` |
| `RateLimited` | `10004` | DIG sync/auth | Per-peer token bucket exhausted |
| `ResourceNotFound` | `10005` | DIG sync/auth | Block / coin / validator not found |
| `SlashingGuardBlocked` | `10010` | DIG crypto | Validator's local slashing DB vetoed the sig |
| `InvalidSignature` | `10011` | DIG crypto | BLS signature failed verification |
| `InvalidProof` | `10012` | DIG crypto | Groth16 / other ZK proof failed |
| `L1Unavailable` | `10020` | DIG L1 | Cannot reach Chia L1 |
| `WalletLocked` | `10030` | DIG wallet | Wallet must unlock before signing |
| `ShutdownPending` | `10040` | DIG lifecycle | Server shutting down |
| `NetworkMismatch` | `10050` | DIG protocol | Client/server network id mismatch |
| `VersionMismatch` | `10051` | DIG protocol | Client/server schema major mismatch |
### Helpers
| `code` | `self -> i32` | Raw numeric value |
| `is_jsonrpc_reserved` | `self -> bool` | In `-32768..=-32000` |
| `is_dig_specific` | `self -> bool` | In `10000..` |
---
## Shared domain types
Module: `dig_rpc_types::types`. `HashHex`, `PubkeyHex`, `SignatureHex`, `Amount`, `BlockSummary`, `ValidatorSummary`, `ValidatorStatus` are re-exported at the crate root.
### Hex-encoded byte arrays
| `HashHex` | 32 | `"0x"` + 64 lowercase hex chars | Block hash, coin id, state root, beacon-block root |
| `PubkeyHex` | 48 | `"0x"` + 96 lowercase hex chars | BLS12-381 G1 compressed pubkey |
| `SignatureHex` | 96 | `"0x"` + 192 lowercase hex chars | BLS12-381 G2 compressed signature |
All three share the same contract:
- **Construct** — `T::new([u8; N])` is `const fn`.
- **Serialize** — `"0x"` + lowercase hex.
- **Deserialize** — accepts upper / lower / mixed case, optional `0x` prefix.
- **`FromStr`** — returns `HexParseError` on length mismatch or invalid hex.
- **`as_bytes() -> &[u8; N]`** — borrow the underlying bytes.
### `HexParseError`
```rust,ignore
pub enum HexParseError {
WrongLength { expected: usize, expected_hex_chars: usize, got: usize },
InvalidHex(hex::FromHexError),
}
```
### `Amount`
```rust,ignore
#[serde(transparent)]
pub struct Amount(pub u64); // mojos; 1 XCH = 10^12 mojos
impl Amount { pub const ZERO: Self = Self(0); }
```
Serialises as a bare JSON number. Safe for amounts up to `2^53 − 1` mojos (~9000 XCH) before JavaScript's float precision kicks in.
### `BlockSummary`
```rust,ignore
pub struct BlockSummary {
pub height: u64,
pub hash: HashHex,
pub parent_hash: HashHex,
pub timestamp: u64,
pub proposer: PubkeyHex,
pub tx_count: u32,
pub weight: u64,
}
```
Returned by `get_block_records` and embedded in `get_blockchain_state`.
### `ValidatorStatus`
```rust,ignore
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum ValidatorStatus {
PendingRegister, Active, ExitingVoluntary, ExitingForced,
Exited, WithdrawalPending, Withdrawn,
}
```
Wire form: `"pending_register"`, `"active"`, etc.
### `ValidatorSummary`
```rust,ignore
pub struct ValidatorSummary {
pub pubkey: PubkeyHex,
pub status: ValidatorStatus,
pub validator_index: Option<u32>,
pub effective_balance: Amount,
pub slashed_amount: Amount,
pub activation_epoch: Option<u64>,
pub exit_epoch: Option<u64>,
}
```
---
## Fullnode methods
Module: `dig_rpc_types::fullnode`. Every method has a `{Method}Request` + `{Method}Response` pair and a `const METHOD: &'static str` on the request.
### Blockchain state
| `get_blockchain_state` | — | `height: u64`, `tip_hash: HashHex`, `synced: bool`, `sync_progress: f32`, `finalized_epoch: u64`, `sealed_height: u64` |
| `get_network_info` | — | `network_id: String`, `genesis_challenge: HashHex`, `chain_age_seconds: u64` |
| `healthz` | — | `ok: bool` |
### Blocks
| `get_block` | `hash: HashHex` | `block: Option<BlockFull>` |
| `get_block_by_height` | `height: u64` | `block: Option<BlockFull>` |
| `get_block_records` | `start_height: u64`, `count: u32` | `records: Vec<BlockSummary>` |
`BlockFull` carries `{header: BlockHeaderWire, body_hex: String, canonical: bool}`. `BlockHeaderWire` carries `{height, hash, parent_hash, timestamp, proposer, state_root, receipts_root, weight, total_iters: u128, signature: SignatureHex}`.
### Coins
| `get_coin_record` | `coin_id: HashHex` | `record: Option<CoinRecordWire>` |
| `get_coin_records_by_hint` | `hint: HashHex`, `include_spent_coins: bool`, `start_height?: u64`, `end_height?: u64` | `records: Vec<CoinRecordWire>` |
| `get_coin_records_by_puzzle_hash` | `puzzle_hash: HashHex`, `include_spent_coins: bool`, `start_height?: u64`, `end_height?: u64` | `records: Vec<CoinRecordWire>` |
`CoinRecordWire = {coin_id, parent_coin_info, puzzle_hash, amount: Amount, confirmed_block_height, spent_block_height, coinbase: bool, timestamp}`.
### Mempool
| `get_mempool` | — | `total_cost: u64`, `total_fees: Amount`, `items: Vec<MempoolItem>` |
| `push_tx` | `spend_bundle_hex: String` | `status: PushTxStatus`, `details?: String` |
`MempoolItem = {spend_bundle_name: HashHex, cost: u64, fee: Amount, is_cpfp: bool}`.
`PushTxStatus` = `"success"` / `"rejected"` / `"already_exists"`.
### Peers
| `get_connections` | — | `peers: Vec<PeerInfoWire>` |
| `ban_peer` | `peer_id: HashHex`, `reason: String`, `duration_secs: u64` | `banned: bool` |
`PeerInfoWire = {peer_id, remote_addr, node_type, connected_since: u64, penalty: u32}`.
### Checkpoint
| `submit_partial_checkpoint_signature` | `epoch: u64`, `validator_index: u32`, `partial_sig: SignatureHex` | `accepted: bool`, `reason?: String` |
| `get_checkpoint_pool` | `epoch?: u64` | `epochs: Vec<CheckpointEpochStatus>` |
`CheckpointEpochStatus = {epoch, partials_count, threshold, aggregated}`.
### Validator set
| `get_validator` | `pubkey: PubkeyHex` | `validator: Option<ValidatorSummary>` |
| `get_active_validators` | `limit: u32`, `offset: u32` | `validators: Vec<ValidatorSummary>`, `total: u32` |
| `get_current_proposer` | `height: u64` | `proposer: Option<ValidatorSummary>` |
### Admin
| `stop_node` | `reason?: String` | `accepted: bool` |
| `get_recovery_status` | — | `mode: RecoveryMode`, `last_anomaly?: String`, `attempts: u32` |
| `get_version` | — | `version: String`, `build_commit: String` |
`RecoveryMode` = `"running"` / `"recovering"` / `"loop_breaker_open"`.
---
## Validator methods
Module: `dig_rpc_types::validator`. Smaller, operator-facing surface.
### Status
| `get_status` | — | `pubkey: PubkeyHex`, `validator_index?: u32`, `can_participate: bool`, `last_duty_at?: u64`, `active_connections: u32` |
| `get_duty_history` | `limit: u32`, `since?: u64` | `entries: Vec<DutyEntry>` |
`DutyEntry = {at: u64, kind: DutyKind, ok: bool, detail?: String}`.
`DutyKind` = `"propose"` / `"attest"` / `"sign_checkpoint"` / `"observe_l1"`.
Helper: `validator::now_unix_seconds() -> u64` for populating `since`.
### Slashing DB
| `get_slashing_db` | — | `last_proposed_slot: u64`, `last_attested_source: u64`, `last_attested_target: u64`, `last_attested_root: HashHex` |
| `export_slashing_db` | `path: String` | `written_bytes: u64` |
| `reset_slashing_db` | `confirm_token: String` | `reset: bool` |
### Admin
| `stop_node` | `reason?: String` | `accepted: bool` |
| `reload_config` | — | `accepted: bool`, `applied_changes: Vec<String>` |
| `healthz` | — | `ok: bool` |
| `get_version` | — | `version: String`, `build_commit: String` |
---
## Stability contract
1. Every enum is `#[non_exhaustive]` — adding variants is additive.
2. [`ErrorCode`](#errorcode) numeric values **never** change once assigned. New variants take the next unused integer.
3. Method names are snake_case and stable.
4. Hex-encoded fields emit `0x` + lowercase; accept case-insensitive + optional `0x` on parse.
5. Response envelopes are valid JSON-RPC 2.0 — exactly one of `result` or `error`.
6. `SCHEMA_VERSION` constant (`"1"`) is bumped on wire-breaking changes.
### Versioning policy
- **Additive changes** (new methods, new optional fields, new enum variants) → bump minor. Older clients continue to work.
- **Removing or reshaping a field** → bump major. Servers may serve multiple majors behind `/v1/`, `/v2/` URL prefixes.
---
## Feature flags
| `client` | off | Optional blocking `reqwest`-backed client (not implemented in v0.1) |
| `schema-export` | off | JSON-Schema dumps via `schemars` for Go/TS codegen |
---
## License
Licensed under either of Apache-2.0 or MIT at your option.