146 lines
4.7 KiB
Rust
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"));
|
|
}
|
|
}
|