dig-rpc-types 0.2.1

Canonical DIG-node JSON-RPC interface: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server logic.
Documentation
//! Conformance vectors: the crate's wire types MUST decode the exact JSON the
//! canonical DIG node (digstore `dig-node` HEAD) emits, and re-encode the same
//! logical shape. Both node crates test against these vectors so the interface
//! and the implementations can never drift.
//!
//! Each vector is a real response/error body taken field-for-field from
//! `handle_rpc` / `download.rs` / `peer.rs` in the canonical node.

use dig_rpc_types::envelope::{JsonRpcResponse, JsonRpcResponseBody};
use dig_rpc_types::error::{ErrorCode, ErrorOrigin, RpcError};
use dig_rpc_types::types::*;
use serde_json::{json, Value};

/// Assert a value deserializes into `T` and re-serializes to a superset-equal
/// JSON (every field in `raw` present + equal in the round-trip).
fn assert_round_trips<T>(raw: Value)
where
    T: serde::de::DeserializeOwned + serde::Serialize,
{
    let decoded: T = serde_json::from_value(raw.clone())
        .unwrap_or_else(|e| panic!("decode failed: {e}\nraw: {raw}"));
    let re = serde_json::to_value(&decoded).unwrap();
    // Every field in the original must appear identically in the round-trip.
    let obj = raw.as_object().expect("vector must be an object");
    for (k, v) in obj {
        assert_eq!(&re[k], v, "field {k:?} drifted on round-trip\ngot: {re}");
    }
}

#[test]
fn get_content_first_window_node_profile() {
    // build_result(resp, 0) — first window, node profile (lib.rs:740-763 + source tag).
    assert_round_trips::<ContentChunk>(json!({
        "ciphertext": "AAAA",
        "root": "ab".repeat(32),
        "complete": false,
        "next_offset": 3_145_728u64,
        "inclusion_proof": "cHJvb2Y=",
        "chunk_lens": [10, 20, 30],
        "source": "local",
    }));
}

#[test]
fn get_content_network_profile() {
    // rpc.dig.net chunk object (dig-rpc.md §3): total_length/length/offset/program_hash.
    assert_round_trips::<ContentChunk>(json!({
        "ciphertext": "AAAA",
        "total_length": 100u64,
        "offset": 0u64,
        "length": 100u64,
        "complete": true,
        "next_offset": Value::Null,
        "inclusion_proof": "cHJvb2Y=",
        "chunk_lens": [],
        "program_hash": "cd".repeat(32),
        "root": "ef".repeat(32),
    }));
}

#[test]
fn anchored_root() {
    // Node::anchored_root result.
    assert_round_trips::<AnchoredRoot>(json!({
        "store_id": "ab".repeat(32),
        "root": "cd".repeat(32),
    }));
}

#[test]
fn network_info() {
    // Node::network_info (lib.rs).
    assert_round_trips::<NetworkInfo>(json!({
        "peer_id": "ab".repeat(32),
        "network_id": "DIG_MAINNET",
        "listen_addr": "[::1]:9444",
        "reflexive_addr": Value::Null,
        "candidate_addresses": ["[::1]:9444", "127.0.0.1:9444"],
        "reachability": "direct",
        "relay": { "url": "wss://relay.dig.net:9450", "reserved": false },
    }));
}

#[test]
fn get_peers_empty() {
    // handle_rpc dig.getPeers base view.
    assert_round_trips::<PeersList>(json!({ "peers": [] }));
}

#[test]
fn announce_ack() {
    // handle_rpc dig.announce base ack.
    assert_round_trips::<AnnounceAck>(json!({ "accepted": true, "known_peers": 0 }));
}

#[test]
fn availability_batch_resource_granularity() {
    // Node::availability_batch — resource answer with providers on a miss.
    assert_round_trips::<AvailabilityBatch>(json!({
        "items": [
            {
                "available": true,
                "total_length": 4096u64,
                "chunk_count": 2u64,
                "complete": true,
            },
            {
                "available": false,
                "providers": [
                    { "peer_id": "12".repeat(32),
                      "addresses": [ { "host": "::1", "port": 9444, "kind": "direct" } ] }
                ],
            }
        ]
    }));
}

#[test]
fn list_inventory_both_shapes() {
    // peer::list_inventory — store view vs all-stores view.
    assert_round_trips::<Inventory>(json!({
        "store_id": "ab".repeat(32),
        "roots": ["cd".repeat(32)],
    }));
    assert_round_trips::<Inventory>(json!({ "stores": ["ef".repeat(32)] }));
}

#[test]
fn fetch_range_first_frame() {
    // FetchedResource::range_frame(0, len) — first frame carries the metadata.
    assert_round_trips::<RangeFrame>(json!({
        "offset": 0u64,
        "length": 1024u64,
        "bytes": "AAAA",
        "complete": false,
        "total_length": 8192u64,
        "chunk_lens": [1024, 1024],
        "chunk_index": 0u64,
        "inclusion_proof": "cHJvb2Y=",
        "root": "ab".repeat(32),
    }));
}

#[test]
fn fetch_range_later_frame() {
    assert_round_trips::<RangeFrame>(json!({
        "offset": 1024u64,
        "length": 1024u64,
        "bytes": "AAAA",
        "complete": true,
    }));
}

#[test]
fn cache_config() {
    // cache.getConfig (lib.rs:2118) — canonical cache_dir field.
    assert_round_trips::<CacheConfig>(json!({
        "cap_bytes": 1_073_741_824u64,
        "used_bytes": 0u64,
        "cache_dir": "/home/u/.cache/dig",
        "shared": true,
    }));
}

#[test]
fn cache_list_cached() {
    // cache.listCached (lib.rs:2158-2175).
    assert_round_trips::<CachedList>(json!({
        "cached": [
            {
                "capsule": format!("{}:{}", "ab".repeat(32), "cd".repeat(32)),
                "store_id": "ab".repeat(32),
                "root": "cd".repeat(32),
                "size_bytes": 4096u64,
                "last_used_unix_ms": 1_700_000_000_000u64,
            }
        ]
    }));
}

#[test]
fn cache_fetch_and_cache_variants() {
    // cache.fetchAndCache success + failed (lib.rs:2200-2210).
    assert_round_trips::<FetchAndCacheResult>(json!({
        "status": "cached",
        "size_bytes": 4096u64,
        "served_root": "ab".repeat(32),
    }));
    assert_round_trips::<FetchAndCacheResult>(json!({
        "status": "failed",
        "message": "upstream miss",
    }));
}

#[test]
fn control_peer_status_not_running() {
    // PeerStatus::snapshot_json on the FFI path.
    assert_round_trips::<PeerStatusSnapshot>(json!({
        "running": false,
        "peer_id": Value::Null,
        "network_id": "DIG_MAINNET",
        "relay": { "url": "wss://relay.dig.net:9450", "reserved": false },
        "connected_peers": 0u64,
        "last_error": Value::Null,
    }));
}

#[test]
fn content_redirect_error_envelope() {
    // download.rs redirect_error_object — the -32008 envelope, decoded as a full
    // JsonRpcResponse error body carrying the redirect payload.
    let raw = json!({
        "jsonrpc": "2.0",
        "id": 7,
        "error": {
            "code": -32008,
            "message": "content not held by this node; re-request against a provider in data.redirect",
            "data": {
                "code": "CONTENT_REDIRECT",
                "origin": "node",
                "redirect": {
                    "content": {
                        "store_id": "ab".repeat(32),
                        "root": "cd".repeat(32),
                        "retrieval_key": "ef".repeat(32),
                    },
                    "providers": [
                        { "peer_id": "12".repeat(32),
                          "addresses": [ { "host": "::1", "port": 9444, "kind": "direct" } ] }
                    ],
                    "redirect_depth": 1u64,
                    "max_redirects": 4u64,
                }
            }
        }
    });
    let resp: JsonRpcResponse = serde_json::from_value(raw).unwrap();
    match resp.body {
        JsonRpcResponseBody::Error { error } => {
            assert_eq!(error.code, ErrorCode::ContentRedirect);
            assert_eq!(error.data.code, "CONTENT_REDIRECT");
            assert_eq!(error.data.origin, ErrorOrigin::Node);
            let redirect = error.data.redirect.expect("redirect payload");
            assert_eq!(redirect.redirect_depth, 1);
            assert_eq!(redirect.max_redirects, 4);
            assert_eq!(redirect.providers.len(), 1);
            assert_eq!(redirect.providers[0].addresses[0].host, "::1");
        }
        _ => panic!("expected error body"),
    }
}

#[test]
fn resource_unavailable_bare_error_upgrades_via_helper() {
    // digstore currently mints bare {code,message}; the crate helper produces the
    // uniform envelope. Assert the helper's output matches the canonical envelope.
    let e = RpcError::of(ErrorCode::ResourceUnavailable, "not held");
    let v = serde_json::to_value(&e).unwrap();
    assert_eq!(v["code"], -32004);
    assert_eq!(v["data"]["code"], "RESOURCE_UNAVAILABLE");
    assert_eq!(v["data"]["origin"], "node");
}

#[test]
fn stage_result() {
    // Node::stage result.
    assert_round_trips::<StageResult>(json!({
        "capsule": format!("{}:{}", "ab".repeat(32), "cd".repeat(32)),
        "store_id": "ab".repeat(32),
        "root": "cd".repeat(32),
        "module_path": "/tmp/x.dig",
        "size": 8192u64,
        "content_address": format!("chia://{}:{}/", "ab".repeat(32), "cd".repeat(32)),
        "files": ["index.html", "style.css"],
        "ephemeral": false,
    }));
}