Files
signer/src/client/rpc.rs
T

146 lines
4.7 KiB
Rust

//! Low-level JSON-RPC 2.0 client — framed send/recv over a transport.
//!
//! Port of the C `signer_client_t` / `signer_client_call`. Builds a
//! `{"id","method","params"}` request, sends it framed, receives the framed
//! response, and splits `result` vs `error`.
use serde_json::{json, Value};
use super::transport::ClientTransport;
/// Low-level signer RPC client. Owns the transport.
pub struct SignerClient {
transport: ClientTransport,
last_error: String,
next_id: u64,
}
impl SignerClient {
/// Wrap an open transport.
pub fn new(transport: ClientTransport) -> Self {
SignerClient {
transport,
last_error: String::new(),
next_id: 1,
}
}
/// Last error message from a failed call.
#[allow(dead_code)]
pub fn last_error(&self) -> &str {
&self.last_error
}
/// Allocate the next request id.
fn next_request_id(&mut self) -> String {
let id = self.next_id.to_string();
self.next_id += 1;
id
}
/// Send a raw JSON-RPC request value and return the parsed response value.
///
/// The caller is responsible for attaching an `auth` field if needed.
pub fn call_raw(&mut self, request: &Value) -> Result<Value, String> {
let request_str = serde_json::to_string(request)
.map_err(|e| format!("request serialize failed: {}", e))?;
self.transport
.send(&request_str)
.map_err(|e| format!("send failed: {}", e))?;
let response_str = self
.transport
.recv()
.map_err(|e| format!("recv failed: {}", e))?;
let response: Value = serde_json::from_str(&response_str)
.map_err(|e| format!("response parse failed: {}", e))?;
Ok(response)
}
/// Call a method with the given params array (no auth).
///
/// On success, returns the `result` value. On error, sets `last_error`
/// and returns `Err(message)`.
pub fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
let id = self.next_request_id();
let request = json!({
"id": id,
"method": method,
"params": params,
});
let response = self.call_raw(&request)?;
self.extract_result(&response)
}
/// Call a method with an auth envelope attached.
pub fn call_with_auth(
&mut self,
method: &str,
params: Value,
auth_privkey: &str,
auth_label: &str,
) -> Result<Value, String> {
let id = self.next_request_id();
let mut request = json!({
"id": id,
"method": method,
"params": params,
});
super::auth::attach_auth(&mut request, auth_privkey, auth_label)?;
let response = self.call_raw(&request)?;
self.extract_result(&response)
}
/// Extract `result` from a JSON-RPC response, or capture the error.
fn extract_result(&mut self, response: &Value) -> Result<Value, String> {
if let Some(err) = response.get("error") {
let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(0);
let msg = err
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown_error");
self.last_error = format!("{} (code {})", msg, code);
Err(self.last_error.clone())
} else if let Some(result) = response.get("result") {
Ok(result.clone())
} else {
self.last_error = "response has neither result nor error".into();
Err(self.last_error.clone())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_result_success() {
let mut client = SignerClient {
transport: ClientTransport::Unix(
std::os::unix::net::UnixStream::pair().unwrap().0,
),
last_error: String::new(),
next_id: 1,
};
let resp = json!({"id":"1","result":"deadbeef"});
let r = client.extract_result(&resp).unwrap();
assert_eq!(r, json!("deadbeef"));
}
#[test]
fn test_extract_result_error() {
let mut client = SignerClient {
transport: ClientTransport::Unix(
std::os::unix::net::UnixStream::pair().unwrap().0,
),
last_error: String::new(),
next_id: 1,
};
let resp = json!({"id":"1","error":{"code":-32601,"message":"method_not_found"}});
let r = client.extract_result(&resp);
assert!(r.is_err());
assert!(client.last_error().contains("method_not_found"));
assert!(client.last_error().contains("-32601"));
}
}