Skip to content

Commit

Permalink
cln-rpc: Scaffolding for the cln-rpc crate
Browse files Browse the repository at this point in the history
  • Loading branch information
cdecker committed Jan 25, 2022
1 parent 680fc62 commit b7bc3c8
Show file tree
Hide file tree
Showing 11 changed files with 923 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[workspace]
members = [
"cln-rpc",
]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ ifneq ($(FUZZING),0)
include tests/fuzz/Makefile
endif
ifneq ($(RUST),0)
# Add Rust Makefiles here
include cln-rpc/Makefile
endif

# We make pretty much everything depend on these.
Expand Down
19 changes: 19 additions & 0 deletions cln-rpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "cln-rpc"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.51"
bytes = "1.1.0"
log = "0.4.14"
serde = { version = "1.0.131", features = ["derive"] }
serde_json = "1.0.72"
tokio-util = { version = "0.6.9", features = ["codec"] }
tokio = { version = "1", features = ["net"]}
native-tls = { version = "*", features = ["vendored"] }
futures-util = { version = "*", features = [ "sink" ] }

[dev-dependencies]
tokio = { version = "1", features = ["net", "macros", "rt-multi-thread"]}
env_logger = "*"
16 changes: 16 additions & 0 deletions cln-rpc/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
cln-rpc-wrongdir:
$(MAKE) -C .. cln-rpc-all

CLN_RPC_EXAMPLES :=
CLN_RPC_GENALL = cln-rpc/src/model.rs
CLN_RPC_SOURCES = $(shell find cln-rpc -name *.rs) ${CLN_RPC_GENALL}
JSON_SCHEMA = doc/schemas/*.schema.json
DEFAULT_TARGETS += $(CLN_RPC_EXAMPLES) $(CLN_RPC_GENALL)

$(CLN_RPC_GENALL): $(JSON_SCHEMA)
PYTHONPATH=contrib/msggen python3 contrib/msggen/msggen/__main__.py

target/debug/examples/cln-rpc-getinfo: $(shell find cln-rpc -name *.rs)
cargo build --example cln-rpc-getinfo

cln-rpc-all: ${CLN_RPC_GEN_ALL} ${CLN_RPC_EXAMPLES}
3 changes: 3 additions & 0 deletions cln-rpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `cln-rpc`: Talk to c-lightning


210 changes: 210 additions & 0 deletions cln-rpc/src/codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//! The codec is used to encode and decode messages received from and
//! sent to the main daemon. The protocol uses `stdout` and `stdin` to
//! exchange JSON formatted messages. Each message is separated by an
//! empty line and we're guaranteed that no other empty line is
//! present in the messages.
use crate::Error;
use anyhow::anyhow;
use bytes::{BufMut, BytesMut};
use serde_json::value::Value;
use std::str::FromStr;
use std::{io, str};
use tokio_util::codec::{Decoder, Encoder};

pub use crate::jsonrpc::JsonRpc;
use crate::{
model::{Request},
notifications::Notification,
};

/// A simple codec that parses messages separated by two successive
/// `\n` newlines.
#[derive(Default)]
pub struct MultiLineCodec {}

/// Find two consecutive newlines, i.e., an empty line, signalling the
/// end of one message and the start of the next message.
fn find_separator(buf: &mut BytesMut) -> Option<usize> {
buf.iter()
.zip(buf.iter().skip(1))
.position(|b| *b.0 == b'\n' && *b.1 == b'\n')
}

fn utf8(buf: &[u8]) -> Result<&str, io::Error> {
str::from_utf8(buf)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Unable to decode input as UTF8"))
}

impl Decoder for MultiLineCodec {
type Item = String;
type Error = Error;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Error> {
if let Some(newline_offset) = find_separator(buf) {
let line = buf.split_to(newline_offset + 2);
let line = &line[..line.len() - 2];
let line = utf8(line)?;
Ok(Some(line.to_string()))
} else {
Ok(None)
}
}
}

impl<T> Encoder<T> for MultiLineCodec
where
T: AsRef<str>,
{
type Error = Error;
fn encode(&mut self, line: T, buf: &mut BytesMut) -> Result<(), Self::Error> {
let line = line.as_ref();
buf.reserve(line.len() + 2);
buf.put(line.as_bytes());
buf.put_u8(b'\n');
buf.put_u8(b'\n');
Ok(())
}
}

#[derive(Default)]
pub struct JsonCodec {
/// Sub-codec used to split the input into chunks that can then be
/// parsed by the JSON parser.
inner: MultiLineCodec,
}

impl<T> Encoder<T> for JsonCodec
where
T: Into<Value>,
{
type Error = Error;
fn encode(&mut self, msg: T, buf: &mut BytesMut) -> Result<(), Self::Error> {
let s = msg.into().to_string();
self.inner.encode(s, buf)
}
}

impl Decoder for JsonCodec {
type Item = Value;
type Error = Error;

fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Error> {
match self.inner.decode(buf) {
Ok(None) => Ok(None),
Err(e) => Err(e),
Ok(Some(s)) => {
if let Ok(v) = Value::from_str(&s) {
Ok(Some(v))
} else {
Err(anyhow!("failed to parse JSON"))
}
}
}
}
}

/// A codec that reads fully formed [crate::messages::JsonRpc]
/// messages. Internally it uses the [JsonCodec] which itself is built
/// on the [MultiLineCodec].
#[derive(Default)]
pub(crate) struct JsonRpcCodec {
inner: JsonCodec,
}

impl Decoder for JsonRpcCodec {
type Item = JsonRpc<Notification, Request>;
type Error = Error;

fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Error> {
match self.inner.decode(buf) {
Ok(None) => Ok(None),
Err(e) => Err(e),
Ok(Some(s)) => {
let req: Self::Item = serde_json::from_value(s)?;
Ok(Some(req))
}
}
}
}

#[cfg(test)]
mod test {
use super::{find_separator, JsonCodec, MultiLineCodec};
use bytes::{BufMut, BytesMut};
use serde_json::json;
use tokio_util::codec::{Decoder, Encoder};

#[test]
fn test_separator() {
struct Test(String, Option<usize>);
let tests = vec![
Test("".to_string(), None),
Test("}\n\n".to_string(), Some(1)),
Test("\"hello\"},\n\"world\"}\n\n".to_string(), Some(18)),
];

for t in tests.iter() {
let mut buf = BytesMut::new();
buf.put_slice(t.0.as_bytes());
assert_eq!(find_separator(&mut buf), t.1);
}
}

#[test]
fn test_ml_decoder() {
struct Test(String, Option<String>, String);
let tests = vec![
Test("".to_string(), None, "".to_string()),
Test(
"{\"hello\":\"world\"}\n\nremainder".to_string(),
Some("{\"hello\":\"world\"}".to_string()),
"remainder".to_string(),
),
Test(
"{\"hello\":\"world\"}\n\n{}\n\nremainder".to_string(),
Some("{\"hello\":\"world\"}".to_string()),
"{}\n\nremainder".to_string(),
),
];

for t in tests.iter() {
let mut buf = BytesMut::new();
buf.put_slice(t.0.as_bytes());

let mut codec = MultiLineCodec::default();
let mut remainder = BytesMut::new();
remainder.put_slice(t.2.as_bytes());

assert_eq!(codec.decode(&mut buf).unwrap(), t.1);
assert_eq!(buf, remainder);
}
}

#[test]
fn test_ml_encoder() {
let tests = vec!["test"];

for t in tests.iter() {
let mut buf = BytesMut::new();
let mut codec = MultiLineCodec::default();
let mut expected = BytesMut::new();
expected.put_slice(t.as_bytes());
expected.put_u8(b'\n');
expected.put_u8(b'\n');
codec.encode(t, &mut buf).unwrap();
assert_eq!(buf, expected);
}
}

#[test]
fn test_json_codec() {
let tests = vec![json!({"hello": "world"})];

for t in tests.iter() {
let mut codec = JsonCodec::default();
let mut buf = BytesMut::new();
codec.encode(t.clone(), &mut buf).unwrap();
let decoded = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(&decoded, t);
}
}
}
88 changes: 88 additions & 0 deletions cln-rpc/src/jsonrpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Common structs to handle JSON-RPC decoding and encoding. They are
//! generic over the Notification and Request types.
use serde::ser::{SerializeStruct, Serializer};
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;

#[derive(Debug)]
pub enum JsonRpc<N, R> {
Request(usize, R),
Notification(N),
}

/// This function disentangles the various cases:
///
/// 1) If we have an `id` then it is a request
///
/// 2) Otherwise it's a notification that doesn't require a
/// response.
///
/// Furthermore we distinguish between the built-in types and the
/// custom user notifications/methods:
///
/// 1) We either match a built-in type above,
///
/// 2) Or it's a custom one, so we pass it around just as a
/// `serde_json::Value`
impl<'de, N, R> Deserialize<'de> for JsonRpc<N, R>
where
N: Deserialize<'de> + Debug,
R: Deserialize<'de> + Debug,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Debug)]
struct IdHelper {
id: Option<usize>,
}

let v = Value::deserialize(deserializer)?;
let helper = IdHelper::deserialize(&v).map_err(de::Error::custom)?;
match helper.id {
Some(id) => {
let r = R::deserialize(v).map_err(de::Error::custom)?;
Ok(JsonRpc::Request(id, r))
}
None => {
let n = N::deserialize(v).map_err(de::Error::custom)?;
Ok(JsonRpc::Notification(n))
}
}
}
}

impl<N, R> Serialize for JsonRpc<N, R>
where
N: Serialize + Debug,
R: Serialize + Debug,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
JsonRpc::Notification(r) => {
let r = serde_json::to_value(r).unwrap();
let mut s = serializer.serialize_struct("Notification", 3)?;
s.serialize_field("jsonrpc", "2.0")?;
s.serialize_field("method", &r["method"])?;
s.serialize_field("params", &r["params"])?;
s.end()
}
JsonRpc::Request(id, r) => {
let r = serde_json::to_value(r).unwrap();
let mut s = serializer.serialize_struct("Request", 4)?;
s.serialize_field("jsonrpc", "2.0")?;
s.serialize_field("id", id)?;
s.serialize_field("method", &r["method"])?;
s.serialize_field("params", &r["params"])?;
s.end()
}
}
}
}
Loading

0 comments on commit b7bc3c8

Please sign in to comment.