Skip to content
This repository was archived by the owner on Nov 26, 2024. It is now read-only.

Commit e0fd89a

Browse files
committed
Import jsonrpc crate
Import the `rust-jsonrpc` crate from https://github.com/apoelstra/rust-jsonrpc using current tip of master `59646e6 Merge apoelstra/rust-jsonrpc#119: Use rust-bitcoin-maintainer-tools and re-write CI` Full commit hash: 59646e6e6ac95f07998133b1709e4a1fa2dbc7bd Do so using the following commands: mkdir jsonrpc mkdir jsonrpc/contrib rsync -avz ../../rust-jsonrpc/master/README.md jsonrpc rsync -avz ../../rust-jsonrpc/master/src jsonrpc rsync -avz ../../rust-jsonrpc/master/contrib/test_vars.sh jsonrpc/contrib Then: - Update `contrib/crates.sh` to include `jsonrpc`. - Remove workspaces from `jsonrpc/Cargo.toml`. - Add `jsonrpc` to repository workspace.
1 parent 4b0bd28 commit e0fd89a

File tree

13 files changed

+2226
-2
lines changed

13 files changed

+2226
-2
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["client", "json"]
2+
members = ["client", "json", "jsonrpc"]
33
exclude = ["integration_test", "regtest"]
44
resolver = "2"
55

contrib/crates.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# shellcheck disable=SC2148
33

44
# Crates in this workspace to test.
5-
CRATES=("json" "client")
5+
CRATES=("json" "client" "jsonrpc")

jsonrpc/Cargo.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[package]
2+
name = "jsonrpc"
3+
version = "0.18.0"
4+
authors = ["Andrew Poelstra <[email protected]>"]
5+
license = "CC0-1.0"
6+
homepage = "https://github.com/apoelstra/rust-jsonrpc/"
7+
repository = "https://github.com/apoelstra/rust-jsonrpc/"
8+
documentation = "https://docs.rs/jsonrpc/"
9+
description = "Rust support for the JSON-RPC 2.0 protocol"
10+
keywords = [ "protocol", "json", "http", "jsonrpc" ]
11+
readme = "README.md"
12+
edition = "2021"
13+
rust-version = "1.56.1"
14+
exclude = ["tests", "contrib"]
15+
16+
[package.metadata.docs.rs]
17+
all-features = true
18+
rustdoc-args = ["--cfg", "docsrs"]
19+
20+
[features]
21+
default = [ "simple_http", "simple_tcp" ]
22+
# A bare-minimum HTTP transport.
23+
simple_http = [ "base64" ]
24+
# A transport that uses `minreq` as the HTTP client.
25+
minreq_http = [ "base64", "minreq" ]
26+
# Basic transport over a raw TcpListener
27+
simple_tcp = []
28+
# Basic transport over a raw UnixStream
29+
simple_uds = []
30+
# Enable Socks5 Proxy in transport
31+
proxy = ["socks"]
32+
33+
[dependencies]
34+
serde = { version = "1", features = ["derive"] }
35+
serde_json = { version = "1", features = [ "raw_value" ] }
36+
37+
base64 = { version = "0.13.0", optional = true }
38+
minreq = { version = "2.7.0", features = ["json-using-serde"], optional = true }
39+
socks = { version = "0.3.4", optional = true}
40+
41+
[lints.rust]
42+
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(jsonrpc_fuzz)'] }

jsonrpc/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[![Status](https://travis-ci.org/apoelstra/rust-jsonrpc.png?branch=master)](https://travis-ci.org/apoelstra/rust-jsonrpc)
2+
3+
# Rust Version compatibility
4+
5+
This library is compatible with Rust **1.63.0** or higher.
6+
7+
# Rust JSONRPC Client
8+
9+
Rudimentary support for sending JSONRPC 2.0 requests and receiving responses.
10+
11+
As an example, hit a local bitcoind JSON-RPC endpoint and call the `uptime` command.
12+
13+
```rust
14+
use jsonrpc::Client;
15+
use jsonrpc::simple_http::{self, SimpleHttpTransport};
16+
17+
fn client(url: &str, user: &str, pass: &str) -> Result<Client, simple_http::Error> {
18+
let t = SimpleHttpTransport::builder()
19+
.url(url)?
20+
.auth(user, Some(pass))
21+
.build();
22+
23+
Ok(Client::with_transport(t))
24+
}
25+
26+
// Demonstrate an example JSON-RCP call against bitcoind.
27+
fn main() {
28+
let client = client("localhost:18443", "user", "pass").expect("failed to create client");
29+
let request = client.build_request("uptime", None);
30+
let response = client.send_request(request).expect("send_request failed");
31+
32+
// For other commands this would be a struct matching the returned json.
33+
let result: u64 = response.result().expect("response is an error, use check_error");
34+
println!("bitcoind uptime: {}", result);
35+
}
36+
```
37+
38+
## Githooks
39+
40+
To assist devs in catching errors _before_ running CI we provide some githooks. If you do not
41+
already have locally configured githooks you can use the ones in this repository by running, in the
42+
root directory of the repository:
43+
```
44+
git config --local core.hooksPath githooks/
45+
```
46+
47+
Alternatively add symlinks in your `.git/hooks` directory to any of the githooks we provide.

jsonrpc/contrib/test_vars.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
3+
# `rust-jsonrpc` does not have a std feature.
4+
FEATURES_WITH_STD=""
5+
6+
# So this is the var to use for all tests.
7+
FEATURES_WITHOUT_STD="simple_http minreq_http simple_tcp simple_uds proxy"
8+
9+
# Run these examples.
10+
EXAMPLES=""

jsonrpc/src/client.rs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! # Client support
4+
//!
5+
//! Support for connecting to JSONRPC servers over HTTP, sending requests,
6+
//! and parsing responses
7+
8+
use std::borrow::Cow;
9+
use std::collections::HashMap;
10+
use std::fmt;
11+
use std::hash::{Hash, Hasher};
12+
use std::sync::atomic;
13+
14+
use serde_json::value::RawValue;
15+
use serde_json::Value;
16+
17+
use crate::error::Error;
18+
use crate::{Request, Response};
19+
20+
/// An interface for a transport over which to use the JSONRPC protocol.
21+
pub trait Transport: Send + Sync + 'static {
22+
/// Sends an RPC request over the transport.
23+
fn send_request(&self, _: Request) -> Result<Response, Error>;
24+
/// Sends a batch of RPC requests over the transport.
25+
fn send_batch(&self, _: &[Request]) -> Result<Vec<Response>, Error>;
26+
/// Formats the target of this transport. I.e. the URL/socket/...
27+
fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result;
28+
}
29+
30+
/// A JSON-RPC client.
31+
///
32+
/// Creates a new Client using one of the transport-specific constructors e.g.,
33+
/// [`Client::simple_http`] for a bare-minimum HTTP transport.
34+
pub struct Client {
35+
pub(crate) transport: Box<dyn Transport>,
36+
nonce: atomic::AtomicUsize,
37+
}
38+
39+
impl Client {
40+
/// Creates a new client with the given transport.
41+
pub fn with_transport<T: Transport>(transport: T) -> Client {
42+
Client { transport: Box::new(transport), nonce: atomic::AtomicUsize::new(1) }
43+
}
44+
45+
/// Builds a request.
46+
///
47+
/// To construct the arguments, one can use one of the shorthand methods
48+
/// [`crate::arg`] or [`crate::try_arg`].
49+
pub fn build_request<'a>(&self, method: &'a str, params: Option<&'a RawValue>) -> Request<'a> {
50+
let nonce = self.nonce.fetch_add(1, atomic::Ordering::Relaxed);
51+
Request { method, params, id: serde_json::Value::from(nonce), jsonrpc: Some("2.0") }
52+
}
53+
54+
/// Sends a request to a client.
55+
pub fn send_request(&self, request: Request) -> Result<Response, Error> {
56+
self.transport.send_request(request)
57+
}
58+
59+
/// Sends a batch of requests to the client.
60+
///
61+
/// Note that the requests need to have valid IDs, so it is advised to create the requests
62+
/// with [`Client::build_request`].
63+
///
64+
/// # Returns
65+
///
66+
/// The return vector holds the response for the request at the corresponding index. If no
67+
/// response was provided, it's [`None`].
68+
pub fn send_batch(&self, requests: &[Request]) -> Result<Vec<Option<Response>>, Error> {
69+
if requests.is_empty() {
70+
return Err(Error::EmptyBatch);
71+
}
72+
73+
// If the request body is invalid JSON, the response is a single response object.
74+
// We ignore this case since we are confident we are producing valid JSON.
75+
let responses = self.transport.send_batch(requests)?;
76+
if responses.len() > requests.len() {
77+
return Err(Error::WrongBatchResponseSize);
78+
}
79+
80+
//TODO(stevenroose) check if the server preserved order to avoid doing the mapping
81+
82+
// First index responses by ID and catch duplicate IDs.
83+
let mut by_id = HashMap::with_capacity(requests.len());
84+
for resp in responses.into_iter() {
85+
let id = HashableValue(Cow::Owned(resp.id.clone()));
86+
if let Some(dup) = by_id.insert(id, resp) {
87+
return Err(Error::BatchDuplicateResponseId(dup.id));
88+
}
89+
}
90+
// Match responses to the requests.
91+
let results =
92+
requests.iter().map(|r| by_id.remove(&HashableValue(Cow::Borrowed(&r.id)))).collect();
93+
94+
// Since we're also just producing the first duplicate ID, we can also just produce the
95+
// first incorrect ID in case there are multiple.
96+
if let Some(id) = by_id.keys().next() {
97+
return Err(Error::WrongBatchResponseId((*id.0).clone()));
98+
}
99+
100+
Ok(results)
101+
}
102+
103+
/// Makes a request and deserializes the response.
104+
///
105+
/// To construct the arguments, one can use one of the shorthand methods
106+
/// [`crate::arg`] or [`crate::try_arg`].
107+
pub fn call<R: for<'a> serde::de::Deserialize<'a>>(
108+
&self,
109+
method: &str,
110+
args: Option<&RawValue>,
111+
) -> Result<R, Error> {
112+
let request = self.build_request(method, args);
113+
let id = request.id.clone();
114+
115+
let response = self.send_request(request)?;
116+
if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) {
117+
return Err(Error::VersionMismatch);
118+
}
119+
if response.id != id {
120+
return Err(Error::NonceMismatch);
121+
}
122+
123+
response.result()
124+
}
125+
}
126+
127+
impl fmt::Debug for crate::Client {
128+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129+
write!(f, "jsonrpc::Client(")?;
130+
self.transport.fmt_target(f)?;
131+
write!(f, ")")
132+
}
133+
}
134+
135+
impl<T: Transport> From<T> for Client {
136+
fn from(t: T) -> Client { Client::with_transport(t) }
137+
}
138+
139+
/// Newtype around `Value` which allows hashing for use as hashmap keys,
140+
/// this is needed for batch requests.
141+
///
142+
/// The reason `Value` does not support `Hash` or `Eq` by itself
143+
/// is that it supports `f64` values; but for batch requests we
144+
/// will only be hashing the "id" field of the request/response
145+
/// pair, which should never need decimal precision and therefore
146+
/// never use `f64`.
147+
#[derive(Clone, PartialEq, Debug)]
148+
struct HashableValue<'a>(pub Cow<'a, Value>);
149+
150+
impl<'a> Eq for HashableValue<'a> {}
151+
152+
impl<'a> Hash for HashableValue<'a> {
153+
fn hash<H: Hasher>(&self, state: &mut H) {
154+
match *self.0.as_ref() {
155+
Value::Null => "null".hash(state),
156+
Value::Bool(false) => "false".hash(state),
157+
Value::Bool(true) => "true".hash(state),
158+
Value::Number(ref n) => {
159+
"number".hash(state);
160+
if let Some(n) = n.as_i64() {
161+
n.hash(state);
162+
} else if let Some(n) = n.as_u64() {
163+
n.hash(state);
164+
} else {
165+
n.to_string().hash(state);
166+
}
167+
}
168+
Value::String(ref s) => {
169+
"string".hash(state);
170+
s.hash(state);
171+
}
172+
Value::Array(ref v) => {
173+
"array".hash(state);
174+
v.len().hash(state);
175+
for obj in v {
176+
HashableValue(Cow::Borrowed(obj)).hash(state);
177+
}
178+
}
179+
Value::Object(ref m) => {
180+
"object".hash(state);
181+
m.len().hash(state);
182+
for (key, val) in m {
183+
key.hash(state);
184+
HashableValue(Cow::Borrowed(val)).hash(state);
185+
}
186+
}
187+
}
188+
}
189+
}
190+
191+
#[cfg(test)]
192+
mod tests {
193+
use std::borrow::Cow;
194+
use std::collections::HashSet;
195+
use std::str::FromStr;
196+
use std::sync;
197+
198+
use super::*;
199+
200+
struct DummyTransport;
201+
impl Transport for DummyTransport {
202+
fn send_request(&self, _: Request) -> Result<Response, Error> { Err(Error::NonceMismatch) }
203+
fn send_batch(&self, _: &[Request]) -> Result<Vec<Response>, Error> { Ok(vec![]) }
204+
fn fmt_target(&self, _: &mut fmt::Formatter) -> fmt::Result { Ok(()) }
205+
}
206+
207+
#[test]
208+
fn sanity() {
209+
let client = Client::with_transport(DummyTransport);
210+
assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 1);
211+
let req1 = client.build_request("test", None);
212+
assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 2);
213+
let req2 = client.build_request("test", None);
214+
assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 3);
215+
assert!(req1.id != req2.id);
216+
}
217+
218+
#[test]
219+
fn hash_value() {
220+
let val = HashableValue(Cow::Owned(Value::from_str("null").unwrap()));
221+
let t = HashableValue(Cow::Owned(Value::from_str("true").unwrap()));
222+
let f = HashableValue(Cow::Owned(Value::from_str("false").unwrap()));
223+
let ns =
224+
HashableValue(Cow::Owned(Value::from_str("[0, -0, 123.4567, -100000000]").unwrap()));
225+
let m =
226+
HashableValue(Cow::Owned(Value::from_str("{ \"field\": 0, \"field\": -0 }").unwrap()));
227+
228+
let mut coll = HashSet::new();
229+
230+
assert!(!coll.contains(&val));
231+
coll.insert(val.clone());
232+
assert!(coll.contains(&val));
233+
234+
assert!(!coll.contains(&t));
235+
assert!(!coll.contains(&f));
236+
coll.insert(t.clone());
237+
assert!(coll.contains(&t));
238+
assert!(!coll.contains(&f));
239+
coll.insert(f.clone());
240+
assert!(coll.contains(&t));
241+
assert!(coll.contains(&f));
242+
243+
assert!(!coll.contains(&ns));
244+
coll.insert(ns.clone());
245+
assert!(coll.contains(&ns));
246+
247+
assert!(!coll.contains(&m));
248+
coll.insert(m.clone());
249+
assert!(coll.contains(&m));
250+
}
251+
}

0 commit comments

Comments
 (0)