mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-20 13:54:36 +01:00
cln-rpc: Scaffolding for the cln-rpc crate
Changelog-Added: cln-rpc: A new Rust library called `cln-rpc` can be used to interact with the JSON-RPC
This commit is contained in:
parent
7fdad0a60c
commit
faa3835177
11 changed files with 933 additions and 1 deletions
4
Cargo.toml
Normal file
4
Cargo.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"cln-rpc",
|
||||
]
|
2
Makefile
2
Makefile
|
@ -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.
|
||||
|
|
19
cln-rpc/Cargo.toml
Normal file
19
cln-rpc/Cargo.toml
Normal file
|
@ -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
cln-rpc/Makefile
Normal file
16
cln-rpc/Makefile
Normal file
|
@ -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
cln-rpc/README.md
Normal file
3
cln-rpc/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `cln-rpc`: Talk to c-lightning
|
||||
|
||||
|
210
cln-rpc/src/codec.rs
Normal file
210
cln-rpc/src/codec.rs
Normal file
|
@ -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
cln-rpc/src/jsonrpc.rs
Normal file
88
cln-rpc/src/jsonrpc.rs
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
108
cln-rpc/src/lib.rs
Normal file
108
cln-rpc/src/lib.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
use crate::codec::JsonCodec;
|
||||
use crate::codec::JsonRpc;
|
||||
use anyhow::{Context, Error, Result};
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use log::{debug, trace};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::net::UnixStream;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
|
||||
pub mod codec;
|
||||
pub mod jsonrpc;
|
||||
pub mod model;
|
||||
pub mod notifications;
|
||||
pub mod primitives;
|
||||
|
||||
pub use crate::{
|
||||
model::{Request, Response},
|
||||
notifications::Notification,
|
||||
};
|
||||
|
||||
///
|
||||
pub struct ClnRpc {
|
||||
next_id: AtomicUsize,
|
||||
|
||||
#[allow(dead_code)]
|
||||
read: FramedRead<OwnedReadHalf, JsonCodec>,
|
||||
write: FramedWrite<OwnedWriteHalf, JsonCodec>,
|
||||
}
|
||||
|
||||
impl ClnRpc {
|
||||
pub async fn new<P>(path: P) -> Result<ClnRpc>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
debug!(
|
||||
"Connecting to socket at {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
ClnRpc::from_stream(UnixStream::connect(path).await?)
|
||||
}
|
||||
|
||||
fn from_stream(stream: UnixStream) -> Result<ClnRpc> {
|
||||
let (read, write) = stream.into_split();
|
||||
|
||||
Ok(ClnRpc {
|
||||
next_id: AtomicUsize::new(1),
|
||||
read: FramedRead::new(read, JsonCodec::default()),
|
||||
write: FramedWrite::new(write, JsonCodec::default()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn call(&mut self, req: Request) -> Result<Response, Error> {
|
||||
trace!("Sending request {:?}", req);
|
||||
|
||||
// Wrap the raw request in a well-formed JSON-RPC outer dict.
|
||||
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
|
||||
let req: JsonRpc<Notification, Request> = JsonRpc::Request(id, req);
|
||||
let req = serde_json::to_value(req)?;
|
||||
let req2 = req.clone();
|
||||
self.write.send(req).await?;
|
||||
|
||||
let mut response = self
|
||||
.read
|
||||
.next()
|
||||
.await
|
||||
.context("no response from lightningd")?
|
||||
.context("reading response from socket")?;
|
||||
trace!("Read response {:?}", response);
|
||||
|
||||
// Annotate the response with the method from the request, so
|
||||
// serde_json knows which variant of [`Request`] should be
|
||||
// used.
|
||||
response["method"] = req2["method"].clone();
|
||||
|
||||
serde_json::from_value(response).context("converting response into enum")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::model::*;
|
||||
use futures_util::StreamExt;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_call() {
|
||||
let req = Request::Getinfo(requests::GetinfoRequest {});
|
||||
let (uds1, uds2) = UnixStream::pair().unwrap();
|
||||
let mut cln = ClnRpc::from_stream(uds1).unwrap();
|
||||
|
||||
let mut read = FramedRead::new(uds2, JsonCodec::default());
|
||||
tokio::task::spawn(async move {
|
||||
cln.call(req).await.unwrap();
|
||||
});
|
||||
|
||||
let read_req = dbg!(read.next().await.unwrap().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
json!({"id": 1, "method": "getinfo", "params": {}, "jsonrpc": "2.0"}),
|
||||
read_req
|
||||
);
|
||||
}
|
||||
}
|
338
cln-rpc/src/model.rs
Normal file
338
cln-rpc/src/model.rs
Normal file
|
@ -0,0 +1,338 @@
|
|||
#![allow(non_camel_case_types)]
|
||||
//! This file was automatically generated using the following command:
|
||||
//!
|
||||
//! ```bash
|
||||
//! msggen
|
||||
//! ```
|
||||
//!
|
||||
//! Do not edit this file, it'll be overwritten. Rather edit the schema that
|
||||
//! this file was generated from
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use requests::*;
|
||||
pub use responses::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "method", content = "params")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Request {
|
||||
Getinfo(requests::GetinfoRequest),
|
||||
ListFunds(requests::ListfundsRequest),
|
||||
ListChannels(requests::ListchannelsRequest),
|
||||
AddGossip(requests::AddgossipRequest),
|
||||
AutoCleanInvoice(requests::AutocleaninvoiceRequest),
|
||||
CheckMessage(requests::CheckmessageRequest),
|
||||
Close(requests::CloseRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "method", content = "result")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Response {
|
||||
Getinfo(responses::GetinfoResponse),
|
||||
ListFunds(responses::ListfundsResponse),
|
||||
ListChannels(responses::ListchannelsResponse),
|
||||
AddGossip(responses::AddgossipResponse),
|
||||
AutoCleanInvoice(responses::AutocleaninvoiceResponse),
|
||||
CheckMessage(responses::CheckmessageResponse),
|
||||
Close(responses::CloseResponse),
|
||||
}
|
||||
|
||||
pub mod requests {
|
||||
#[allow(unused_imports)]
|
||||
use crate::primitives::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde::{{Deserialize, Serialize}};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GetinfoRequest {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListfundsRequest {
|
||||
#[serde(alias = "spent", skip_serializing_if = "Option::is_none")]
|
||||
pub spent: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListchannelsRequest {
|
||||
#[serde(alias = "short_channel_id", skip_serializing_if = "Option::is_none")]
|
||||
pub short_channel_id: Option<String>,
|
||||
#[serde(alias = "source", skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<String>,
|
||||
#[serde(alias = "destination", skip_serializing_if = "Option::is_none")]
|
||||
pub destination: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AddgossipRequest {
|
||||
#[serde(alias = "message")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AutocleaninvoiceRequest {
|
||||
#[serde(alias = "expired_by", skip_serializing_if = "Option::is_none")]
|
||||
pub expired_by: Option<u64>,
|
||||
#[serde(alias = "cycle_seconds", skip_serializing_if = "Option::is_none")]
|
||||
pub cycle_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CheckmessageRequest {
|
||||
#[serde(alias = "message")]
|
||||
pub message: String,
|
||||
#[serde(alias = "zbase")]
|
||||
pub zbase: String,
|
||||
#[serde(alias = "pubkey", skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CloseRequest {
|
||||
#[serde(alias = "id")]
|
||||
pub id: String,
|
||||
#[serde(alias = "unilateraltimeout", skip_serializing_if = "Option::is_none")]
|
||||
pub unilateraltimeout: Option<u32>,
|
||||
#[serde(alias = "destination", skip_serializing_if = "Option::is_none")]
|
||||
pub destination: Option<String>,
|
||||
#[serde(alias = "fee_negotiation_step", skip_serializing_if = "Option::is_none")]
|
||||
pub fee_negotiation_step: Option<String>,
|
||||
#[serde(alias = "wrong_funding", skip_serializing_if = "Option::is_none")]
|
||||
pub wrong_funding: Option<String>,
|
||||
#[serde(alias = "force_lease_closed", skip_serializing_if = "Option::is_none")]
|
||||
pub force_lease_closed: Option<bool>,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub mod responses {
|
||||
#[allow(unused_imports)]
|
||||
use crate::primitives::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde::{{Deserialize, Serialize}};
|
||||
|
||||
/// Type of connection
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GetinfoAddressType {
|
||||
DNS,
|
||||
IPV4,
|
||||
IPV6,
|
||||
TORV2,
|
||||
TORV3,
|
||||
WEBSOCKET,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GetinfoAddress {
|
||||
// Path `Getinfo.address[].type`
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: GetinfoAddressType,
|
||||
#[serde(alias = "port")]
|
||||
pub port: u16,
|
||||
#[serde(alias = "address", skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
}
|
||||
|
||||
/// Type of connection
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GetinfoBindingType {
|
||||
LOCAL_SOCKET,
|
||||
IPV4,
|
||||
IPV6,
|
||||
TORV2,
|
||||
TORV3,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GetinfoBinding {
|
||||
// Path `Getinfo.binding[].type`
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: GetinfoBindingType,
|
||||
#[serde(alias = "address", skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
#[serde(alias = "port", skip_serializing_if = "Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
#[serde(alias = "socket", skip_serializing_if = "Option::is_none")]
|
||||
pub socket: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct GetinfoResponse {
|
||||
#[serde(alias = "id")]
|
||||
pub id: String,
|
||||
#[serde(alias = "alias")]
|
||||
pub alias: String,
|
||||
#[serde(alias = "color")]
|
||||
pub color: String,
|
||||
#[serde(alias = "num_peers")]
|
||||
pub num_peers: u32,
|
||||
#[serde(alias = "num_pending_channels")]
|
||||
pub num_pending_channels: u32,
|
||||
#[serde(alias = "num_active_channels")]
|
||||
pub num_active_channels: u32,
|
||||
#[serde(alias = "num_inactive_channels")]
|
||||
pub num_inactive_channels: u32,
|
||||
#[serde(alias = "version")]
|
||||
pub version: String,
|
||||
#[serde(alias = "lightning-dir")]
|
||||
pub lightning_dir: String,
|
||||
#[serde(alias = "blockheight")]
|
||||
pub blockheight: u32,
|
||||
#[serde(alias = "network")]
|
||||
pub network: String,
|
||||
#[serde(alias = "fees_collected_msat")]
|
||||
pub fees_collected_msat: Amount,
|
||||
#[serde(alias = "address")]
|
||||
pub address: Vec<GetinfoAddress>,
|
||||
#[serde(alias = "binding")]
|
||||
pub binding: Vec<GetinfoBinding>,
|
||||
#[serde(alias = "warning_bitcoind_sync", skip_serializing_if = "Option::is_none")]
|
||||
pub warning_bitcoind_sync: Option<String>,
|
||||
#[serde(alias = "warning_lightningd_sync", skip_serializing_if = "Option::is_none")]
|
||||
pub warning_lightningd_sync: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ListfundsOutputsStatus {
|
||||
UNCONFIRMED,
|
||||
CONFIRMED,
|
||||
SPENT,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListfundsOutputs {
|
||||
#[serde(alias = "txid")]
|
||||
pub txid: String,
|
||||
#[serde(alias = "output")]
|
||||
pub output: u32,
|
||||
#[serde(alias = "amount_msat")]
|
||||
pub amount_msat: Amount,
|
||||
#[serde(alias = "scriptpubkey")]
|
||||
pub scriptpubkey: String,
|
||||
#[serde(alias = "address", skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<String>,
|
||||
#[serde(alias = "redeemscript", skip_serializing_if = "Option::is_none")]
|
||||
pub redeemscript: Option<String>,
|
||||
// Path `ListFunds.outputs[].status`
|
||||
#[serde(rename = "status")]
|
||||
pub status: ListfundsOutputsStatus,
|
||||
#[serde(alias = "blockheight", skip_serializing_if = "Option::is_none")]
|
||||
pub blockheight: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListfundsChannels {
|
||||
#[serde(alias = "peer_id")]
|
||||
pub peer_id: String,
|
||||
#[serde(alias = "our_amount_msat")]
|
||||
pub our_amount_msat: Amount,
|
||||
#[serde(alias = "amount_msat")]
|
||||
pub amount_msat: Amount,
|
||||
#[serde(alias = "funding_txid")]
|
||||
pub funding_txid: String,
|
||||
#[serde(alias = "funding_output")]
|
||||
pub funding_output: u32,
|
||||
#[serde(alias = "connected")]
|
||||
pub connected: bool,
|
||||
// Path `ListFunds.channels[].state`
|
||||
#[serde(rename = "state")]
|
||||
pub state: ChannelState,
|
||||
#[serde(alias = "short_channel_id", skip_serializing_if = "Option::is_none")]
|
||||
pub short_channel_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListfundsResponse {
|
||||
#[serde(alias = "outputs")]
|
||||
pub outputs: Vec<ListfundsOutputs>,
|
||||
#[serde(alias = "channels")]
|
||||
pub channels: Vec<ListfundsChannels>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListchannelsChannels {
|
||||
#[serde(alias = "source")]
|
||||
pub source: String,
|
||||
#[serde(alias = "destination")]
|
||||
pub destination: String,
|
||||
#[serde(alias = "public")]
|
||||
pub public: bool,
|
||||
#[serde(alias = "amount_msat")]
|
||||
pub amount_msat: Amount,
|
||||
#[serde(alias = "message_flags")]
|
||||
pub message_flags: u8,
|
||||
#[serde(alias = "channel_flags")]
|
||||
pub channel_flags: u8,
|
||||
#[serde(alias = "active")]
|
||||
pub active: bool,
|
||||
#[serde(alias = "last_update")]
|
||||
pub last_update: u32,
|
||||
#[serde(alias = "base_fee_millisatoshi")]
|
||||
pub base_fee_millisatoshi: u32,
|
||||
#[serde(alias = "fee_per_millionth")]
|
||||
pub fee_per_millionth: u32,
|
||||
#[serde(alias = "delay")]
|
||||
pub delay: u32,
|
||||
#[serde(alias = "htlc_minimum_msat")]
|
||||
pub htlc_minimum_msat: Amount,
|
||||
#[serde(alias = "htlc_maximum_msat", skip_serializing_if = "Option::is_none")]
|
||||
pub htlc_maximum_msat: Option<Amount>,
|
||||
#[serde(alias = "features")]
|
||||
pub features: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ListchannelsResponse {
|
||||
#[serde(alias = "channels")]
|
||||
pub channels: Vec<ListchannelsChannels>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AddgossipResponse {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AutocleaninvoiceResponse {
|
||||
#[serde(alias = "enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(alias = "expired_by", skip_serializing_if = "Option::is_none")]
|
||||
pub expired_by: Option<u64>,
|
||||
#[serde(alias = "cycle_seconds", skip_serializing_if = "Option::is_none")]
|
||||
pub cycle_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CheckmessageResponse {
|
||||
#[serde(alias = "verified")]
|
||||
pub verified: bool,
|
||||
#[serde(alias = "pubkey", skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<String>,
|
||||
}
|
||||
|
||||
/// Whether we successfully negotiated a mutual close, closed without them, or discarded not-yet-opened channel
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CloseType {
|
||||
MUTUAL,
|
||||
UNILATERAL,
|
||||
UNOPENED,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CloseResponse {
|
||||
// Path `Close.type`
|
||||
#[serde(rename = "type")]
|
||||
pub item_type: CloseType,
|
||||
#[serde(alias = "tx", skip_serializing_if = "Option::is_none")]
|
||||
pub tx: Option<String>,
|
||||
#[serde(alias = "txid", skip_serializing_if = "Option::is_none")]
|
||||
pub txid: Option<String>,
|
||||
}
|
||||
|
||||
}
|
||||
|
4
cln-rpc/src/notifications.rs
Normal file
4
cln-rpc/src/notifications.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Notification {}
|
142
cln-rpc/src/primitives.rs
Normal file
142
cln-rpc/src/primitives.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserializer, Serializer};
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum ChannelState {
|
||||
OPENINGD,
|
||||
CHANNELD_AWAITING_LOCKIN,
|
||||
CHANNELD_NORMAL,
|
||||
CHANNELD_SHUTTING_DOWN,
|
||||
CLOSINGD_SIGEXCHANGE,
|
||||
CLOSINGD_COMPLETE,
|
||||
AWAITING_UNILATERAL,
|
||||
FUNDING_SPEND_SEEN,
|
||||
ONCHAIN,
|
||||
DUALOPEND_OPEN_INIT,
|
||||
DUALOPEND_AWAITING_LOCKIN,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum ChannelStateChangeCause {
|
||||
UNKNOWN,
|
||||
LOCAL,
|
||||
USER,
|
||||
REMOTE,
|
||||
PROTOCOL,
|
||||
ONCHAIN,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct Amount {
|
||||
msat: u64,
|
||||
}
|
||||
|
||||
impl Amount {
|
||||
pub fn from_msat(msat: u64) -> Amount {
|
||||
Amount { msat: msat }
|
||||
}
|
||||
pub fn from_sat(sat: u64) -> Amount {
|
||||
Amount { msat: 1_000 * sat }
|
||||
}
|
||||
pub fn from_btc(btc: u64) -> Amount {
|
||||
Amount {
|
||||
msat: 100_000_000_000 * btc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum ChannelSide {
|
||||
LOCAL,
|
||||
REMOTE,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Amount {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
let s: String = Deserialize::deserialize(deserializer)?;
|
||||
let ss: &str = &s;
|
||||
ss.try_into()
|
||||
.map_err(|_e| Error::custom("could not parse amount"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Amount {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{}msat", self.msat))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Amount {
|
||||
type Error = Error;
|
||||
fn try_from(s: &str) -> Result<Amount> {
|
||||
let number: u64 = s
|
||||
.chars()
|
||||
.map(|c| c.to_digit(10))
|
||||
.take_while(|opt| opt.is_some())
|
||||
.fold(0, |acc, digit| acc * 10 + (digit.unwrap() as u64));
|
||||
|
||||
let s = s.to_lowercase();
|
||||
if s.ends_with("msat") {
|
||||
Ok(Amount::from_msat(number))
|
||||
} else if s.ends_with("sat") {
|
||||
Ok(Amount::from_sat(number))
|
||||
} else if s.ends_with("btc") {
|
||||
Ok(Amount::from_btc(number))
|
||||
} else {
|
||||
Err(anyhow!("Unable to parse amount from string: {}", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for String {
|
||||
fn from(a: Amount) -> String {
|
||||
format!("{}msat", a.msat)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_amount_serde() {
|
||||
#[derive(Serialize, PartialEq, Debug, Deserialize)]
|
||||
struct T {
|
||||
amount: Amount,
|
||||
}
|
||||
|
||||
let tests = vec![
|
||||
("{\"amount\": \"10msat\"}", Amount { msat: 10 }, "10msat"),
|
||||
(
|
||||
"{\"amount\": \"42sat\"}",
|
||||
Amount { msat: 42_000 },
|
||||
"42000msat",
|
||||
),
|
||||
(
|
||||
"{\"amount\": \"31337btc\"}",
|
||||
Amount {
|
||||
msat: 3_133_700_000_000_000,
|
||||
},
|
||||
"3133700000000000msat",
|
||||
),
|
||||
];
|
||||
|
||||
for (req, res, s) in tests.into_iter() {
|
||||
println!("{:?} {:?}", req, res);
|
||||
let parsed: T = serde_json::from_str(req).unwrap();
|
||||
assert_eq!(res, parsed.amount);
|
||||
|
||||
let serialized: String = parsed.amount.into();
|
||||
assert_eq!(s, serialized);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue