Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSPS5 implementation #3662

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lightning-liquidity/Cargo.toml
Original file line number Diff line number Diff line change
@@ -14,8 +14,9 @@ categories = ["cryptography::cryptocurrencies"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["std"]
default = ["std", "time"]
std = ["lightning/std"]
time = []
backtrace = ["dep:backtrace"]

[dependencies]
18 changes: 17 additions & 1 deletion lightning-liquidity/src/events.rs
Original file line number Diff line number Diff line change
@@ -18,8 +18,8 @@
use crate::lsps0;
use crate::lsps1;
use crate::lsps2;
use crate::lsps5;
use crate::sync::{Arc, Mutex};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's keep this whitespace line.

use alloc::collections::VecDeque;
use alloc::vec::Vec;

@@ -118,6 +118,10 @@ pub enum LiquidityEvent {
LSPS2Client(lsps2::event::LSPS2ClientEvent),
/// An LSPS2 (JIT Channel) server event.
LSPS2Service(lsps2::event::LSPS2ServiceEvent),
/// An LSPS5 (Webhook) client event.
LSPS5Client(lsps5::event::LSPS5ClientEvent),
/// An LSPS5 (Webhook) server event.
LSPS5Service(lsps5::event::LSPS5ServiceEvent),
}

impl From<lsps0::event::LSPS0ClientEvent> for LiquidityEvent {
@@ -151,6 +155,18 @@ impl From<lsps2::event::LSPS2ServiceEvent> for LiquidityEvent {
}
}

impl From<lsps5::event::LSPS5ClientEvent> for LiquidityEvent {
fn from(event: lsps5::event::LSPS5ClientEvent) -> Self {
Self::LSPS5Client(event)
}
}

impl From<lsps5::event::LSPS5ServiceEvent> for LiquidityEvent {
fn from(event: lsps5::event::LSPS5ServiceEvent) -> Self {
Self::LSPS5Service(event)
}
}

struct EventFuture {
event_queue: Arc<Mutex<VecDeque<LiquidityEvent>>>,
waker: Arc<Mutex<Option<Waker>>>,
4 changes: 4 additions & 0 deletions lightning-liquidity/src/lib.rs
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@
//! an LSP will open a "just-in-time" channel. This is useful for the initial on-boarding of
//! clients as the channel opening fees are deducted from the incoming payment, i.e., no funds are
//! required client-side to initiate this flow.
//! - [bLIP-55 / LSPS5] defines a protocol for sending webhook notifications to clients. This is
//! useful for notifying clients about incoming payments, channel expiries, etc.
//!
//! To get started, you'll want to setup a [`LiquidityManager`] and configure it to be the
//! [`CustomMessageHandler`] of your LDK node. You can then for example call
@@ -37,6 +39,7 @@
//! [bLIP-50 / LSPS0]: https://github.com/lightning/blips/blob/master/blip-0050.md
//! [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md
//! [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md
//! [bLIP-55 / LSPS5]: https://github.com/lightning/blips/pull/55/files
//! [`CustomMessageHandler`]: lightning::ln::peer_handler::CustomMessageHandler
//! [`LiquidityManager::next_event`]: crate::LiquidityManager::next_event
#![deny(missing_docs)]
@@ -59,6 +62,7 @@ pub mod events;
pub mod lsps0;
pub mod lsps1;
pub mod lsps2;
pub mod lsps5;
mod manager;
pub mod message_queue;
#[allow(dead_code)]
1 change: 1 addition & 0 deletions lightning-liquidity/src/lsps0/msgs.rs
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@ impl TryFrom<LSPSMessage> for LSPS0Message {
LSPSMessage::LSPS0(message) => Ok(message),
LSPSMessage::LSPS1(_) => Err(()),
LSPSMessage::LSPS2(_) => Err(()),
LSPSMessage::LSPS5(_) => Err(()),
}
}
}
160 changes: 160 additions & 0 deletions lightning-liquidity/src/lsps0/ser.rs
Original file line number Diff line number Diff line change
@@ -21,14 +21,21 @@ use crate::lsps1::msgs::{
use crate::lsps2::msgs::{
LSPS2Message, LSPS2Request, LSPS2Response, LSPS2_BUY_METHOD_NAME, LSPS2_GET_INFO_METHOD_NAME,
};
use crate::lsps5::msgs::{
LSPS5Message, LSPS5Request, LSPS5Response, LSPS5_LIST_WEBHOOKS_METHOD_NAME,
LSPS5_REMOVE_WEBHOOK_METHOD_NAME, LSPS5_SET_WEBHOOK_METHOD_NAME,
};

use crate::prelude::HashMap;

use chrono::DateTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think importing this is fine (although not doing so was a choice before), but now we started, let's drop the chrono:: prefix below.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the style used before. Deleting the import...

use lightning::ln::msgs::{DecodeError, LightningError};
use lightning::ln::wire;
use lightning::util::ser::{LengthLimitedRead, LengthReadable, WithoutLength};

use bitcoin::secp256k1::PublicKey;

use core::time::Duration;
#[cfg(feature = "std")]
use std::time::{SystemTime, UNIX_EPOCH};

@@ -60,6 +67,9 @@ pub(crate) enum LSPSMethod {
LSPS1CreateOrder,
LSPS2GetInfo,
LSPS2Buy,
LSPS5SetWebhook,
LSPS5ListWebhooks,
LSPS5RemoveWebhook,
}

impl LSPSMethod {
@@ -71,6 +81,9 @@ impl LSPSMethod {
Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME,
Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME,
Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME,
Self::LSPS5SetWebhook => LSPS5_SET_WEBHOOK_METHOD_NAME,
Self::LSPS5ListWebhooks => LSPS5_LIST_WEBHOOKS_METHOD_NAME,
Self::LSPS5RemoveWebhook => LSPS5_REMOVE_WEBHOOK_METHOD_NAME,
}
}
}
@@ -85,6 +98,9 @@ impl FromStr for LSPSMethod {
LSPS1_GET_ORDER_METHOD_NAME => Ok(Self::LSPS1GetOrder),
LSPS2_GET_INFO_METHOD_NAME => Ok(Self::LSPS2GetInfo),
LSPS2_BUY_METHOD_NAME => Ok(Self::LSPS2Buy),
LSPS5_SET_WEBHOOK_METHOD_NAME => Ok(Self::LSPS5SetWebhook),
LSPS5_LIST_WEBHOOKS_METHOD_NAME => Ok(Self::LSPS5ListWebhooks),
LSPS5_REMOVE_WEBHOOK_METHOD_NAME => Ok(Self::LSPS5RemoveWebhook),
_ => Err(&"Unknown method name"),
}
}
@@ -117,6 +133,16 @@ impl From<&LSPS2Request> for LSPSMethod {
}
}

impl From<&LSPS5Request> for LSPSMethod {
fn from(value: &LSPS5Request) -> Self {
match value {
LSPS5Request::SetWebhook(_) => Self::LSPS5SetWebhook,
LSPS5Request::ListWebhooks(_) => Self::LSPS5ListWebhooks,
LSPS5Request::RemoveWebhook(_) => Self::LSPS5RemoveWebhook,
}
}
}

impl<'de> Deserialize<'de> for LSPSMethod {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -214,6 +240,16 @@ impl LSPSDateTime {
self.0.timestamp().try_into().expect("expiration to be ahead of unix epoch");
now_seconds_since_epoch > datetime_seconds_since_epoch
}

/// Returns the time in seconds since the unix epoch.
pub fn abs_diff(&self, other: &Self) -> u64 {
self.0.timestamp().abs_diff(other.0.timestamp())
}

/// Returns the time in seconds since the unix epoch.
pub fn new_from_duration_since_epoch(duration: Duration) -> Self {
Self(DateTime::UNIX_EPOCH + duration)
}
}

impl FromStr for LSPSDateTime {
@@ -255,6 +291,8 @@ pub enum LSPSMessage {
LSPS1(LSPS1Message),
/// An LSPS2 message.
LSPS2(LSPS2Message),
/// An LSPS5 message.
LSPS5(LSPS5Message),
}

impl LSPSMessage {
@@ -282,6 +320,10 @@ impl LSPSMessage {
LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => {
Some((LSPSRequestId(request_id.0.clone()), request.into()))
},
// Add LSPS5
LSPSMessage::LSPS5(LSPS5Message::Request(request_id, request)) => {
Some((LSPSRequestId(request_id.0.clone()), request.into()))
},
_ => None,
}
}
@@ -398,6 +440,47 @@ impl Serialize for LSPSMessage {
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &serde_json::Value::Null)?;
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, &error)?;
},
LSPSMessage::LSPS5(LSPS5Message::Request(request_id, request)) => {
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?;
jsonrpc_object
.serialize_field(JSONRPC_METHOD_FIELD_KEY, &LSPSMethod::from(request))?;

match request {
LSPS5Request::SetWebhook(params) => {
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
},
LSPS5Request::ListWebhooks(params) => {
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
},
LSPS5Request::RemoveWebhook(params) => {
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
},
}
},
LSPSMessage::LSPS5(LSPS5Message::Response(request_id, response)) => {
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?;

match response {
LSPS5Response::SetWebhook(result) => {
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
},
LSPS5Response::SetWebhookError(error) => {
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
},
LSPS5Response::ListWebhooks(result) => {
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
},
LSPS5Response::ListWebhooksError(error) => {
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
},
LSPS5Response::RemoveWebhook(result) => {
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
},
LSPS5Response::RemoveWebhookError(error) => {
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
},
}
},
}

jsonrpc_object.end()
@@ -511,6 +594,31 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> {
.map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS2(LSPS2Message::Request(id, LSPS2Request::Buy(request))))
},
// Add LSPS5 methods
LSPSMethod::LSPS5SetWebhook => {
let request = serde_json::from_value(params.unwrap_or(json!({})))
.map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Request(
id,
LSPS5Request::SetWebhook(request),
)))
},
LSPSMethod::LSPS5ListWebhooks => {
let request = serde_json::from_value(params.unwrap_or(json!({})))
.map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Request(
id,
LSPS5Request::ListWebhooks(request),
)))
},
LSPSMethod::LSPS5RemoveWebhook => {
let request = serde_json::from_value(params.unwrap_or(json!({})))
.map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Request(
id,
LSPS5Request::RemoveWebhook(request),
)))
},
},
None => match self.request_id_to_method_map.remove(&id) {
Some(method) => match method {
@@ -616,6 +724,58 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> {
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
}
},
// Add LSPS5 methods
LSPSMethod::LSPS5SetWebhook => {
if let Some(error) = error {
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::SetWebhookError(error),
)))
} else if let Some(result) = result {
let response =
serde_json::from_value(result).map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::SetWebhook(response),
)))
} else {
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
}
},
LSPSMethod::LSPS5ListWebhooks => {
if let Some(error) = error {
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::ListWebhooksError(error),
)))
} else if let Some(result) = result {
let response =
serde_json::from_value(result).map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::ListWebhooks(response),
)))
} else {
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
}
},
LSPSMethod::LSPS5RemoveWebhook => {
if let Some(error) = error {
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::RemoveWebhookError(error),
)))
} else if let Some(result) = result {
let response =
serde_json::from_value(result).map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::RemoveWebhook(response),
)))
} else {
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
}
},
},
None => Err(de::Error::custom(format!(
"Received response for unknown request id: {}",
Loading