Skip to content

LSPS5 implementation #3662

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d64e8d5
Separate time functionality into dedicated feature flag
martinsaposnic Mar 27, 2025
b43d53d
Prefactor - Improvements on LSPSDateTime
martinsaposnic Mar 27, 2025
07cfade
Add custom URL parser for LSPS5.
martinsaposnic Mar 20, 2025
69abc99
fixup: Use UntrustedString and simplify LSPSUrl API
martinsaposnic Apr 17, 2025
31969ed
Add messaging types for webhook operations
martinsaposnic Mar 11, 2025
adb1903
fixup: use UntrustedString for app/webhook, Visitor for deserialization
martinsaposnic Apr 17, 2025
1be2c9f
Add LSPS5 event enums for webhook operations
martinsaposnic Mar 11, 2025
0c0dac5
fixup: LSPS5 events - Improve type safety and documentation
martinsaposnic Apr 17, 2025
e31be89
Add LSPS5 webhook service implementation
martinsaposnic Mar 24, 2025
2e11ed7
fixup: Refactor LSPS5 service: use LSPSDateTime, cleanups, fixes
martinsaposnic Apr 17, 2025
eef8b76
Add LSPS5 webhook client implementation
martinsaposnic Mar 24, 2025
70d9bf2
fixup: Refactors in Client
martinsaposnic Apr 17, 2025
2a909d1
Add LSPS5 module structure
martinsaposnic Mar 24, 2025
90e1e8e
Integrate LSPS5 with liquidity manager
martinsaposnic Mar 24, 2025
3fbb8fb
Add tests for LSPS5 client and service. Also tests for checking the c…
martinsaposnic Mar 24, 2025
0e3ea87
ldk-node needs both LSPS5ClientConfig and LSPS5ServiceConfig to have …
martinsaposnic Apr 28, 2025
004c5c3
WIP
martinsaposnic May 5, 2025
e143d43
WIP
martinsaposnic May 5, 2025
2d84bdb
WIP
martinsaposnic May 5, 2025
4e8664a
wip
martinsaposnic May 6, 2025
fd8f2fa
WIP
martinsaposnic May 6, 2025
3971d83
WIP
martinsaposnic May 6, 2025
67ef22e
WIP
martinsaposnic May 6, 2025
909dfe1
wip
martinsaposnic May 6, 2025
dec5cba
wip
martinsaposnic May 6, 2025
8db198c
wip
martinsaposnic May 6, 2025
11ee263
WIP
martinsaposnic May 6, 2025
4ba9a59
WIP
martinsaposnic May 6, 2025
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
4 changes: 2 additions & 2 deletions lightning-background-processor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
futures = [ ]
std = ["lightning/std", "lightning-liquidity/std", "bitcoin-io/std", "bitcoin_hashes/std"]

default = ["std"]
time = ["std"]
default = ["std", "time"]

[dependencies]
bitcoin = { version = "0.32.2", default-features = false }
Expand Down
21 changes: 18 additions & 3 deletions lightning-background-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ impl<
G,
&'a (dyn UtxoLookup + Send + Sync),
L,
> where
>
where
L::Target: Logger,
{
/// Initializes a new [`GossipSync::Rapid`] variant.
Expand All @@ -247,7 +248,8 @@ impl<'a, L: Deref>
&'a NetworkGraph<L>,
&'a (dyn UtxoLookup + Send + Sync),
L,
> where
>
where
L::Target: Logger,
{
/// Initializes a new [`GossipSync::None`] variant.
Expand Down Expand Up @@ -1142,6 +1144,7 @@ mod tests {
use lightning::util::sweep::{OutputSpendStatus, OutputSweeper, PRUNE_DELAY_BLOCKS};
use lightning::util::test_utils;
use lightning::{get_event, get_event_msg};
use lightning_liquidity::lsps5::service::TimeProvider;
use lightning_liquidity::LiquidityManager;
use lightning_persister::fs_store::FilesystemStore;
use lightning_rapid_gossip_sync::RapidGossipSync;
Expand Down Expand Up @@ -1578,6 +1581,16 @@ mod tests {
path.to_str().unwrap().to_string()
}

pub struct DefaultTimeProvider;

#[cfg(feature = "std")]
impl TimeProvider for DefaultTimeProvider {
fn duration_since_epoch(&self) -> Duration {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch")
}
}

fn create_nodes(num_nodes: usize, persist_dir: &str) -> (String, Vec<Node>) {
let persist_temp_path = env::temp_dir().join(persist_dir);
let persist_dir = persist_temp_path.to_string_lossy().to_string();
Expand Down Expand Up @@ -1676,13 +1689,15 @@ mod tests {
logger.clone(),
keys_manager.clone(),
));
let liquidity_manager = Arc::new(LiquidityManager::new(
let time_provider = Arc::new(DefaultTimeProvider);
let liquidity_manager = Arc::new(LiquidityManager::new_with_custom_time_provider(
Arc::clone(&keys_manager),
Arc::clone(&manager),
None,
None,
None,
None,
time_provider,
));
let node = Node {
node: manager,
Expand Down
3 changes: 2 additions & 1 deletion lightning-liquidity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ["std"]
backtrace = ["dep:backtrace"]

[dependencies]
Expand Down
17 changes: 17 additions & 0 deletions lightning-liquidity/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub use event_queue::MAX_EVENT_QUEUE_SIZE;
use crate::lsps0;
use crate::lsps1;
use crate::lsps2;
use crate::lsps5;

/// An event which you should probably take some action in response to.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -38,6 +39,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 {
Expand Down Expand Up @@ -70,3 +75,15 @@ impl From<lsps2::event::LSPS2ServiceEvent> for LiquidityEvent {
Self::LSPS2Service(event)
}
}

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)
}
}
4 changes: 4 additions & 0 deletions lightning-liquidity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)]
Expand All @@ -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)]
Expand Down
1 change: 1 addition & 0 deletions lightning-liquidity/src/lsps0/msgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl TryFrom<LSPSMessage> for LSPS0Message {
LSPSMessage::LSPS0(message) => Ok(message),
LSPSMessage::LSPS1(_) => Err(()),
LSPSMessage::LSPS2(_) => Err(()),
LSPSMessage::LSPS5(_) => Err(()),
}
}
}
Expand Down
156 changes: 156 additions & 0 deletions lightning-liquidity/src/lsps0/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ 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 lightning::ln::msgs::{DecodeError, LightningError};
Expand All @@ -29,6 +34,7 @@ use lightning::util::ser::{LengthLimitedRead, LengthReadable, WithoutLength};

use bitcoin::secp256k1::PublicKey;

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

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

impl LSPSMethod {
Expand All @@ -71,6 +80,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,
}
}
}
Expand All @@ -85,6 +97,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"),
}
}
Expand Down Expand Up @@ -117,6 +132,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
Expand Down Expand Up @@ -214,6 +239,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(chrono::DateTime::UNIX_EPOCH + duration)
}
}

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

impl LSPSMessage {
Expand Down Expand Up @@ -282,6 +319,9 @@ impl LSPSMessage {
LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => {
Some((LSPSRequestId(request_id.0.clone()), request.into()))
},
LSPSMessage::LSPS5(LSPS5Message::Request(request_id, request)) => {
Some((LSPSRequestId(request_id.0.clone()), request.into()))
},
_ => None,
}
}
Expand Down Expand Up @@ -398,6 +438,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()
Expand Down Expand Up @@ -511,6 +592,30 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> {
.map_err(de::Error::custom)?;
Ok(LSPSMessage::LSPS2(LSPS2Message::Request(id, LSPS2Request::Buy(request))))
},
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 {
Expand Down Expand Up @@ -616,6 +721,57 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> {
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
}
},
LSPSMethod::LSPS5SetWebhook => {
if let Some(error) = error {
Ok(LSPSMessage::LSPS5(LSPS5Message::Response(
id,
LSPS5Response::SetWebhookError(error.into()),
)))
} 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.into()),
)))
} 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.into()),
)))
} 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: {}",
Expand Down
Loading
Loading