use serde_json::{json, Value};
use crate::error::ErrorCode;
use crate::method::Method;
pub const OPENRPC_VERSION: &str = "1.2.6";
pub fn openrpc_document(version: &str) -> Value {
json!({
"openrpc": OPENRPC_VERSION,
"info": {
"title": "DIG Node RPC",
"version": version,
"description":
"The canonical DIG-node JSON-RPC surface: content reads, peer \
discovery + range serving, and loopback control. Tiers and \
error codes are defined by the dig-rpc-protocol crate.",
},
"methods": Method::ALL.iter().map(|m| method_object(*m)).collect::<Vec<_>>(),
"components": {
"x-dig-errors": ErrorCode::ALL.iter().map(|c| json!({
"machineCode": c.machine_code(),
"code": c.code(),
"message": c.default_message(),
"origin": c.default_origin(),
})).collect::<Vec<_>>(),
},
"x-dig-peer-allowlist": Method::peer_reachable_names(),
})
}
fn method_object(m: Method) -> Value {
json!({
"name": m.name(),
"summary": m.summary(),
"params": [],
"result": {
"name": "result",
"schema": { "type": "object" },
},
"x-dig-tier": m.tier().as_str(),
"x-dig-peer-reachable": m.is_peer_reachable(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn document_header() {
let doc = openrpc_document("9.9.9");
assert_eq!(doc["openrpc"], "1.2.6");
assert_eq!(doc["info"]["version"], "9.9.9");
}
#[test]
fn all_methods_present_with_extensions() {
let doc = openrpc_document("1.0.0");
let methods = doc["methods"].as_array().unwrap();
assert_eq!(methods.len(), Method::ALL.len());
for m in Method::ALL {
let obj = methods
.iter()
.find(|o| o["name"] == m.name())
.unwrap_or_else(|| panic!("missing method {}", m.name()));
assert_eq!(obj["x-dig-tier"], m.tier().as_str());
assert_eq!(obj["x-dig-peer-reachable"], m.is_peer_reachable());
}
}
#[test]
fn error_catalogue_complete() {
let doc = openrpc_document("1.0.0");
let errors = doc["components"]["x-dig-errors"].as_array().unwrap();
assert_eq!(errors.len(), ErrorCode::ALL.len());
assert!(errors
.iter()
.any(|e| e["code"] == -32020 && e["machineCode"] == "ONION_CIRCUIT_UNAVAILABLE"));
assert!(errors
.iter()
.any(|e| e["code"] == -32030 && e["machineCode"] == "UNAUTHORIZED"));
}
#[test]
fn reward_methods_are_emitted_as_control_and_off_the_peer_allowlist() {
let doc = openrpc_document("1.0.0");
let methods = doc["methods"].as_array().unwrap();
let allow = doc["x-dig-peer-allowlist"].as_array().unwrap();
let reward: Vec<Method> = Method::ALL
.iter()
.copied()
.filter(|m| m.is_reward())
.collect();
assert!(
!reward.is_empty(),
"no method reported is_reward() -- this assertion would be vacuous"
);
assert!(
reward.contains(&Method::GetPayeeRewardClaimStatus),
"dig.getPayeeRewardClaimStatus must be on the reward surface"
);
for m in reward {
let name = m.name();
let obj = methods
.iter()
.find(|o| o["name"] == name)
.unwrap_or_else(|| panic!("{name} is missing from the OpenRPC document"));
assert_eq!(
obj["x-dig-tier"], "control",
"{name} must be emitted as control"
);
assert_eq!(
obj["x-dig-peer-reachable"], false,
"{name} must be emitted as not peer-reachable"
);
assert!(
!allow.iter().any(|n| n == name),
"{name} must not appear in the emitted peer allowlist"
);
assert!(
!obj["summary"].as_str().unwrap().is_empty(),
"{name} must carry a summary in the discovery document"
);
}
}
#[test]
fn peer_allowlist_extension() {
let doc = openrpc_document("1.0.0");
let allow = doc["x-dig-peer-allowlist"].as_array().unwrap();
assert_eq!(allow.len(), Method::peer_reachable_names().len());
assert!(allow.iter().any(|n| n == "dig.getContent"));
assert!(!allow.iter().any(|n| n == "cache.clear"));
}
}