dig-rpc-types 0.1.0

JSON-RPC wire types shared by the DIG Network fullnode + validator RPC servers and their clients. Pure types — no I/O, no async, no logic.
Documentation
//! Stable JSON-RPC error codes.
//!
//! # Contract
//!
//! Once a variant is assigned a numeric value, that value **never** changes.
//! New variants append to the enum at the next unused integer. The enum is
//! `#[non_exhaustive]` so clients built against an older version don't
//! panic on a newer server's new code.
//!
//! # Ranges
//!
//! - `-32768..=-32000` — reserved by JSON-RPC 2.0. We use the spec-defined
//!   pre-defined values (`ParseError`, `InvalidRequest`, `MethodNotFound`,
//!   `InvalidParams`, `InternalError`).
//! - `10000..` — DIG-reserved. Sub-allocated by concern:
//!   - `10001..=10009` — sync / auth / permission
//!   - `10010..=10019` — cryptographic checks
//!   - `10020..=10029` — L1 (Chia) issues
//!   - `10030..=10039` — wallet state
//!   - `10040..=10049` — lifecycle
//!   - `10050..=10059` — protocol / version
//!
//! # References
//!
//! - [JSON-RPC 2.0 spec §5.1](https://www.jsonrpc.org/specification#error_object)
//! - [serde_repr](https://docs.rs/serde_repr) — the crate providing the
//!   `Serialize_repr` / `Deserialize_repr` derive macros used here.

use serde_repr::{Deserialize_repr, Serialize_repr};

/// Stable JSON-RPC error code.
///
/// Serialises as a bare integer on the wire (via `serde_repr`) so values
/// are JSON-RPC-spec-compliant. The enum is `#[non_exhaustive]` so that
/// adding variants in minor releases is backwards-compatible — clients
/// built against an older `dig-rpc-types` must use `_ => ...` when
/// matching.
#[repr(i32)]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq, Hash)]
pub enum ErrorCode {
    // ---------- JSON-RPC 2.0 reserved range ----------
    /// Invalid JSON was received by the server.
    ParseError = -32700,
    /// The JSON sent is not a valid Request object.
    InvalidRequest = -32600,
    /// The method does not exist / is not available.
    MethodNotFound = -32601,
    /// Invalid method parameter(s).
    InvalidParams = -32602,
    /// Internal JSON-RPC error.
    InternalError = -32603,

    // ---------- DIG reserved 1xxxx range ----------
    /// The fullnode is not yet synced to chain tip; the requested data
    /// may be stale or unavailable.
    NotSynced = 10001,
    /// The peer's certificate does not resolve to any known role (internal
    /// server only).
    PeerUnauthorized = 10002,
    /// The peer's resolved role is below the method's `min_role`.
    PermissionDenied = 10003,
    /// The peer exceeded its per-opcode token bucket.
    RateLimited = 10004,
    /// The requested block / coin / validator was not found.
    ResourceNotFound = 10005,

    /// The validator's local slashing-protection DB would veto this
    /// signature (e.g., proposed-slot watermark, attested-target watermark).
    SlashingGuardBlocked = 10010,
    /// A provided BLS signature failed verification.
    InvalidSignature = 10011,
    /// A provided Groth16 or other ZK proof failed verification.
    InvalidProof = 10012,

    /// Cannot reach Chia L1 (peer pool exhausted, no coinset fallback).
    L1Unavailable = 10020,

    /// The wallet is locked; unlock before calling signing methods.
    WalletLocked = 10030,

    /// Server is shutting down; retry against another peer.
    ShutdownPending = 10040,

    /// The client's advertised network id differs from the server's.
    NetworkMismatch = 10050,
    /// The client's advertised schema major version differs from the server's.
    VersionMismatch = 10051,
}

impl ErrorCode {
    /// Return the raw integer code.
    ///
    /// Equivalent to `self as i32`; provided as an explicit method so
    /// callers don't rely on repr casting.
    pub fn code(self) -> i32 {
        self as i32
    }

    /// Whether this variant is in the JSON-RPC-reserved range.
    pub fn is_jsonrpc_reserved(self) -> bool {
        let c = self.code();
        (-32768..=-32000).contains(&c)
    }

    /// Whether this variant is DIG-specific (1xxxx range).
    pub fn is_dig_specific(self) -> bool {
        self.code() >= 10000
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **Proves:** every variant's numeric value is pinned to the value
    /// declared in the enum — that is, [`ErrorCode::MethodNotFound as i32`]
    /// equals `-32601`.
    ///
    /// **Why it matters:** Once published, error codes are a permanent part
    /// of the wire contract. Reordering the enum variants or switching
    /// `#[repr]` types would change the on-wire integers and break every
    /// deployed client.
    ///
    /// **Catches:** a mass rename-or-reorder of the enum that accidentally
    /// renumbers variants; removing `#[repr(i32)]`; switching to a
    /// different representation.
    #[test]
    fn numeric_values_pinned() {
        assert_eq!(ErrorCode::ParseError as i32, -32700);
        assert_eq!(ErrorCode::InvalidRequest as i32, -32600);
        assert_eq!(ErrorCode::MethodNotFound as i32, -32601);
        assert_eq!(ErrorCode::InvalidParams as i32, -32602);
        assert_eq!(ErrorCode::InternalError as i32, -32603);
        assert_eq!(ErrorCode::NotSynced as i32, 10001);
        assert_eq!(ErrorCode::PeerUnauthorized as i32, 10002);
        assert_eq!(ErrorCode::PermissionDenied as i32, 10003);
        assert_eq!(ErrorCode::RateLimited as i32, 10004);
        assert_eq!(ErrorCode::ResourceNotFound as i32, 10005);
        assert_eq!(ErrorCode::SlashingGuardBlocked as i32, 10010);
        assert_eq!(ErrorCode::InvalidSignature as i32, 10011);
        assert_eq!(ErrorCode::InvalidProof as i32, 10012);
        assert_eq!(ErrorCode::L1Unavailable as i32, 10020);
        assert_eq!(ErrorCode::WalletLocked as i32, 10030);
        assert_eq!(ErrorCode::ShutdownPending as i32, 10040);
        assert_eq!(ErrorCode::NetworkMismatch as i32, 10050);
        assert_eq!(ErrorCode::VersionMismatch as i32, 10051);
    }

    /// **Proves:** serialisation produces a bare integer (not a tagged enum
    /// object).
    ///
    /// **Why it matters:** JSON-RPC 2.0 mandates that `error.code` is a
    /// number. A tagged form (`{"MethodNotFound": null}`) would violate the
    /// spec and break every standards-compliant client library.
    ///
    /// **Catches:** a regression where `Serialize_repr` is replaced with
    /// plain `Serialize` (which would produce `"MethodNotFound"` string).
    #[test]
    fn serialises_as_integer() {
        let s = serde_json::to_string(&ErrorCode::MethodNotFound).unwrap();
        assert_eq!(s, "-32601");

        let s = serde_json::to_string(&ErrorCode::NotSynced).unwrap();
        assert_eq!(s, "10001");
    }

    /// **Proves:** `is_jsonrpc_reserved` / `is_dig_specific` correctly
    /// classify variants by their numeric range.
    ///
    /// **Why it matters:** Middleware may want to treat the two classes
    /// differently (log DIG-specific errors with more context). Pinning
    /// the classification prevents subtle bugs where a new variant lands
    /// in the wrong range.
    ///
    /// **Catches:** adding a new variant in the wrong range (e.g.,
    /// `-32500` — outside the reserved JSON-RPC range and also outside
    /// the DIG range).
    #[test]
    fn range_classifiers_work() {
        assert!(ErrorCode::MethodNotFound.is_jsonrpc_reserved());
        assert!(!ErrorCode::MethodNotFound.is_dig_specific());

        assert!(ErrorCode::NotSynced.is_dig_specific());
        assert!(!ErrorCode::NotSynced.is_jsonrpc_reserved());
    }
}