mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-02-23 22:56:54 +01:00
Merge pull request #3436 from tnull/2024-12-add-lightning-liquidity-crate
Add `lightning-liquidity` crate to the workspace
This commit is contained in:
commit
6ad40f996a
39 changed files with 8056 additions and 4 deletions
|
@ -16,6 +16,7 @@ members = [
|
|||
"lightning-transaction-sync",
|
||||
"lightning-macros",
|
||||
"lightning-dns-resolver",
|
||||
"lightning-liquidity",
|
||||
"possiblyrandom",
|
||||
]
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ PIN_RELEASE_DEPS # pin the release dependencies in our main workspace
|
|||
# Starting with version 0.5.9 (there is no .6-.8), the `home` crate has an MSRV of rustc 1.70.0.
|
||||
[ "$RUSTC_MINOR_VERSION" -lt 70 ] && cargo update -p home --precise "0.5.5" --verbose
|
||||
|
||||
# proptest 1.3.0 requires rustc 1.64.0
|
||||
[ "$RUSTC_MINOR_VERSION" -lt 64 ] && cargo update -p proptest --precise "1.2.0" --verbose
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
|
||||
echo -e "\n\nChecking the full workspace."
|
||||
|
@ -58,6 +61,7 @@ WORKSPACE_MEMBERS=(
|
|||
lightning-transaction-sync
|
||||
lightning-macros
|
||||
lightning-dns-resolver
|
||||
lightning-liquidity
|
||||
possiblyrandom
|
||||
)
|
||||
|
||||
|
@ -110,7 +114,7 @@ echo -e "\n\nTest backtrace-debug builds"
|
|||
cargo test -p lightning --verbose --color always --features backtrace
|
||||
|
||||
echo -e "\n\nTesting no_std builds"
|
||||
for DIR in lightning-invoice lightning-rapid-gossip-sync; do
|
||||
for DIR in lightning-invoice lightning-rapid-gossip-sync lightning-liquidity; do
|
||||
cargo test -p $DIR --verbose --color always --no-default-features
|
||||
done
|
||||
|
||||
|
|
50
lightning-liquidity/Cargo.toml
Normal file
50
lightning-liquidity/Cargo.toml
Normal file
|
@ -0,0 +1,50 @@
|
|||
[package]
|
||||
name = "lightning-liquidity"
|
||||
version = "0.1.0-alpha.6"
|
||||
authors = ["John Cantrell <johncantrell97@gmail.com>", "Elias Rohrer <dev@tnull.de>"]
|
||||
homepage = "https://lightningdevkit.org/"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
description = "Types and primitives to integrate a spec-compliant LSP with an LDK-based node."
|
||||
repository = "https://github.com/lightningdevkit/lightning-liquidity/"
|
||||
readme = "README.md"
|
||||
keywords = ["bitcoin", "lightning", "ldk", "bdk"]
|
||||
categories = ["cryptography::cryptocurrencies"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["lightning/std"]
|
||||
backtrace = ["dep:backtrace"]
|
||||
|
||||
[dependencies]
|
||||
lightning = { version = "0.0.124", path = "../lightning", default-features = false }
|
||||
lightning-types = { version = "0.1", path = "../lightning-types", default-features = false }
|
||||
lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default-features = false, features = ["serde"] }
|
||||
|
||||
bitcoin = { version = "0.32.2", default-features = false, features = ["serde"] }
|
||||
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde", "alloc"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
|
||||
serde_json = "1.0"
|
||||
backtrace = { version = "0.3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
lightning = { version = "0.0.124", path = "../lightning", default-features = false, features = ["_test_utils"] }
|
||||
lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default-features = false, features = ["serde", "std"] }
|
||||
lightning-persister = { version = "0.0.124", path = "../lightning-persister", default-features = false }
|
||||
lightning-background-processor = { version = "0.0.124", path = "../lightning-background-processor", default-features = false, features = ["std"] }
|
||||
|
||||
proptest = "1.0.0"
|
||||
tokio = { version = "1.35", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] }
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "forbid"
|
||||
# When adding a new cfg attribute, ensure that it is added to this list.
|
||||
check-cfg = [
|
||||
"cfg(lsps1_service)",
|
||||
"cfg(c_bindings)",
|
||||
"cfg(backtrace)",
|
||||
"cfg(ldk_bench)",
|
||||
]
|
20
lightning-liquidity/README.md
Normal file
20
lightning-liquidity/README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# lightning-liquidity
|
||||
|
||||
The goal of this crate is to provide types and primitives to integrate a spec-compliant LSP with an LDK-based node. To this end, this crate provides client-side as well as service-side logic to implement the [LSP specifications].
|
||||
|
||||
Currently the following specifications are supported:
|
||||
- [LSPS0] defines the transport protocol with the LSP over which the other protocols communicate.
|
||||
- [LSPS1] allows to order Lightning channels from an LSP. This is useful when the client needs
|
||||
inbound Lightning liquidity for which they are willing and able to pay in bitcoin.
|
||||
- [LSPS2] allows to generate a special invoice for which, when paid, an LSP will open a "just-in-time".
|
||||
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.
|
||||
|
||||
To get started, you'll want to setup a `LiquidityManager` and configure it to be the `CustomMessageHandler` of your LDK node. You can then call `LiquidityManager::lsps1_client_handler` / `LiquidityManager::lsps2_client_handler`, or `LiquidityManager::lsps2_service_handler`, to access the respective client-side or service-side handlers.
|
||||
|
||||
`LiquidityManager` uses an eventing system to notify the user about important updates to the protocol flow. To this end, you will need to handle events emitted via one of the event handling methods provided by `LiquidityManager`, e.g., `LiquidityManager::next_event`.
|
||||
|
||||
[LSP specifications]: https://github.com/BitcoinAndLightningLayerSpecs/lsp
|
||||
[LSPS0]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0
|
||||
[LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1
|
||||
[LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2
|
245
lightning-liquidity/src/events.rs
Normal file
245
lightning-liquidity/src/events.rs
Normal file
|
@ -0,0 +1,245 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Events are surfaced by the library to indicate some action must be taken
|
||||
//! by the end-user.
|
||||
//!
|
||||
//! Because we don't have a built-in runtime, it's up to the end-user to poll
|
||||
//! [`LiquidityManager::get_and_clear_pending_events`] to receive events.
|
||||
//!
|
||||
//! [`LiquidityManager::get_and_clear_pending_events`]: crate::LiquidityManager::get_and_clear_pending_events
|
||||
|
||||
use crate::lsps0;
|
||||
use crate::lsps1;
|
||||
use crate::lsps2;
|
||||
use crate::prelude::{Vec, VecDeque};
|
||||
use crate::sync::{Arc, Mutex};
|
||||
|
||||
use core::future::Future;
|
||||
use core::task::{Poll, Waker};
|
||||
|
||||
/// The maximum queue size we allow before starting to drop events.
|
||||
pub const MAX_EVENT_QUEUE_SIZE: usize = 1000;
|
||||
|
||||
pub(crate) struct EventQueue {
|
||||
queue: Arc<Mutex<VecDeque<Event>>>,
|
||||
waker: Arc<Mutex<Option<Waker>>>,
|
||||
#[cfg(feature = "std")]
|
||||
condvar: crate::sync::Condvar,
|
||||
}
|
||||
|
||||
impl EventQueue {
|
||||
pub fn new() -> Self {
|
||||
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
||||
let waker = Arc::new(Mutex::new(None));
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let condvar = crate::sync::Condvar::new();
|
||||
Self { queue, waker, condvar }
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
Self { queue, waker }
|
||||
}
|
||||
|
||||
pub fn enqueue(&self, event: Event) {
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
if queue.len() < MAX_EVENT_QUEUE_SIZE {
|
||||
queue.push_back(event);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(waker) = self.waker.lock().unwrap().take() {
|
||||
waker.wake();
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
self.condvar.notify_one();
|
||||
}
|
||||
|
||||
pub fn next_event(&self) -> Option<Event> {
|
||||
self.queue.lock().unwrap().pop_front()
|
||||
}
|
||||
|
||||
pub async fn next_event_async(&self) -> Event {
|
||||
EventFuture { event_queue: Arc::clone(&self.queue), waker: Arc::clone(&self.waker) }.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn wait_next_event(&self) -> Event {
|
||||
let mut queue = self
|
||||
.condvar
|
||||
.wait_while(self.queue.lock().unwrap(), |queue: &mut VecDeque<Event>| queue.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let event = queue.pop_front().expect("non-empty queue");
|
||||
let should_notify = !queue.is_empty();
|
||||
|
||||
drop(queue);
|
||||
|
||||
if should_notify {
|
||||
if let Some(waker) = self.waker.lock().unwrap().take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
self.condvar.notify_one();
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
pub fn get_and_clear_pending_events(&self) -> Vec<Event> {
|
||||
self.queue.lock().unwrap().split_off(0).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// An event which you should probably take some action in response to.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
/// An LSPS0 client event.
|
||||
LSPS0Client(lsps0::event::LSPS0ClientEvent),
|
||||
/// An LSPS1 (Channel Request) client event.
|
||||
LSPS1Client(lsps1::event::LSPS1ClientEvent),
|
||||
/// An LSPS1 (Channel Request) server event.
|
||||
#[cfg(lsps1_service)]
|
||||
LSPS1Service(lsps1::event::LSPS1ServiceEvent),
|
||||
/// An LSPS2 (JIT Channel) client event.
|
||||
LSPS2Client(lsps2::event::LSPS2ClientEvent),
|
||||
/// An LSPS2 (JIT Channel) server event.
|
||||
LSPS2Service(lsps2::event::LSPS2ServiceEvent),
|
||||
}
|
||||
|
||||
struct EventFuture {
|
||||
event_queue: Arc<Mutex<VecDeque<Event>>>,
|
||||
waker: Arc<Mutex<Option<Waker>>>,
|
||||
}
|
||||
|
||||
impl Future for EventFuture {
|
||||
type Output = Event;
|
||||
|
||||
fn poll(
|
||||
self: core::pin::Pin<&mut Self>, cx: &mut core::task::Context<'_>,
|
||||
) -> core::task::Poll<Self::Output> {
|
||||
if let Some(event) = self.event_queue.lock().unwrap().pop_front() {
|
||||
Poll::Ready(event)
|
||||
} else {
|
||||
*self.waker.lock().unwrap() = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "std")]
|
||||
async fn event_queue_works() {
|
||||
use super::*;
|
||||
use crate::lsps0::event::LSPS0ClientEvent;
|
||||
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
|
||||
use core::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
let event_queue = Arc::new(EventQueue::new());
|
||||
assert_eq!(event_queue.next_event(), None);
|
||||
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let counterparty_node_id =
|
||||
PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let expected_event = Event::LSPS0Client(LSPS0ClientEvent::ListProtocolsResponse {
|
||||
counterparty_node_id,
|
||||
protocols: Vec::new(),
|
||||
});
|
||||
|
||||
for _ in 0..3 {
|
||||
event_queue.enqueue(expected_event.clone());
|
||||
}
|
||||
|
||||
assert_eq!(event_queue.wait_next_event(), expected_event);
|
||||
assert_eq!(event_queue.next_event_async().await, expected_event);
|
||||
assert_eq!(event_queue.next_event(), Some(expected_event.clone()));
|
||||
assert_eq!(event_queue.next_event(), None);
|
||||
|
||||
// Check `next_event_async` won't return if the queue is empty and always rather timeout.
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(10)) => {
|
||||
// Timeout
|
||||
}
|
||||
_ = event_queue.next_event_async() => {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
assert_eq!(event_queue.next_event(), None);
|
||||
|
||||
// Check we get the expected number of events when polling/enqueuing concurrently.
|
||||
let enqueued_events = AtomicU16::new(0);
|
||||
let received_events = AtomicU16::new(0);
|
||||
let mut delayed_enqueue = false;
|
||||
|
||||
for _ in 0..25 {
|
||||
event_queue.enqueue(expected_event.clone());
|
||||
enqueued_events.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(10)), if !delayed_enqueue => {
|
||||
event_queue.enqueue(expected_event.clone());
|
||||
enqueued_events.fetch_add(1, Ordering::SeqCst);
|
||||
delayed_enqueue = true;
|
||||
}
|
||||
e = event_queue.next_event_async() => {
|
||||
assert_eq!(e, expected_event);
|
||||
received_events.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
event_queue.enqueue(expected_event.clone());
|
||||
enqueued_events.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
e = event_queue.next_event_async() => {
|
||||
assert_eq!(e, expected_event);
|
||||
received_events.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
if delayed_enqueue
|
||||
&& received_events.load(Ordering::SeqCst) == enqueued_events.load(Ordering::SeqCst)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(event_queue.next_event(), None);
|
||||
|
||||
// Check we operate correctly, even when mixing and matching blocking and async API calls.
|
||||
let (tx, mut rx) = tokio::sync::watch::channel(());
|
||||
let thread_queue = Arc::clone(&event_queue);
|
||||
let thread_event = expected_event.clone();
|
||||
std::thread::spawn(move || {
|
||||
let e = thread_queue.wait_next_event();
|
||||
assert_eq!(e, thread_event);
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
let thread_queue = Arc::clone(&event_queue);
|
||||
let thread_event = expected_event.clone();
|
||||
std::thread::spawn(move || {
|
||||
// Sleep a bit before we enqueue the events everybody is waiting for.
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
thread_queue.enqueue(thread_event.clone());
|
||||
thread_queue.enqueue(thread_event.clone());
|
||||
});
|
||||
|
||||
let e = event_queue.next_event_async().await;
|
||||
assert_eq!(e, expected_event.clone());
|
||||
|
||||
rx.changed().await.unwrap();
|
||||
assert_eq!(event_queue.next_event(), None);
|
||||
}
|
||||
}
|
72
lightning-liquidity/src/lib.rs
Normal file
72
lightning-liquidity/src/lib.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
#![crate_name = "lightning_liquidity"]
|
||||
|
||||
//! The goal of this crate is to provide types and primitives to integrate a spec-compliant LSP with an LDK-based node. To this end, this crate provides client-side as well as service-side logic to implement the [LSP specifications].
|
||||
//!
|
||||
//! Currently the following specifications are supported:
|
||||
//! - [LSPS0] defines the transport protocol with the LSP over which the other protocols communicate.
|
||||
//! - [LSPS1] allows to order Lightning channels from an LSP. This is useful when the client needs
|
||||
//! inbound Lightning liquidity for which they are willing and able to pay in bitcoin.
|
||||
//! - [LSPS2] allows to generate a special invoice for which, when paid, an LSP will open a "just-in-time".
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
//! [`LiquidityManager::lsps1_client_handler`] / [`LiquidityManager::lsps2_client_handler`], or
|
||||
//! [`LiquidityManager::lsps2_service_handler`], to access the respective client-side or
|
||||
//! service-side handlers.
|
||||
//!
|
||||
//! [`LiquidityManager`] uses an eventing system to notify the user about important updates to the
|
||||
//! protocol flow. To this end, you will need to handle events emitted via one of the event
|
||||
//! handling methods provided by [`LiquidityManager`], e.g., [`LiquidityManager::next_event`].
|
||||
//!
|
||||
//! [LSP specifications]: https://github.com/BitcoinAndLightningLayerSpecs/lsp
|
||||
//! [LSPS0]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0
|
||||
//! [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1
|
||||
//! [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2
|
||||
//! [`CustomMessageHandler`]: lightning::ln::peer_handler::CustomMessageHandler
|
||||
//! [`LiquidityManager::next_event`]: crate::LiquidityManager::next_event
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
#![deny(rustdoc::private_intra_doc_links)]
|
||||
#![allow(bare_trait_objects)]
|
||||
#![allow(ellipsis_inclusive_range_patterns)]
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
mod prelude {
|
||||
#![allow(unused_imports)]
|
||||
pub use alloc::{boxed::Box, collections::VecDeque, string::String, vec, vec::Vec};
|
||||
|
||||
pub use alloc::borrow::ToOwned;
|
||||
pub use alloc::string::ToString;
|
||||
|
||||
pub(crate) use lightning::util::hash_tables::*;
|
||||
}
|
||||
|
||||
pub mod events;
|
||||
pub mod lsps0;
|
||||
pub mod lsps1;
|
||||
pub mod lsps2;
|
||||
mod manager;
|
||||
pub mod message_queue;
|
||||
#[allow(dead_code)]
|
||||
#[allow(unused_imports)]
|
||||
mod sync;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
pub use manager::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig};
|
155
lightning-liquidity/src/lsps0/client.rs
Normal file
155
lightning-liquidity/src/lsps0/client.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
//! Contains the main LSPS2 client-side object, [`LSPS0ClientHandler`].
|
||||
//!
|
||||
//! Please refer to the [LSPS0
|
||||
//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more
|
||||
//! information.
|
||||
|
||||
use crate::events::{Event, EventQueue};
|
||||
use crate::lsps0::event::LSPS0ClientEvent;
|
||||
use crate::lsps0::msgs::{
|
||||
LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsRequest, ListProtocolsResponse,
|
||||
};
|
||||
use crate::lsps0::ser::{ProtocolMessageHandler, ResponseError};
|
||||
use crate::message_queue::MessageQueue;
|
||||
use crate::sync::Arc;
|
||||
use crate::utils;
|
||||
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::sign::EntropySource;
|
||||
use lightning::util::logger::Level;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
use core::ops::Deref;
|
||||
|
||||
/// A message handler capable of sending and handling LSPS0 messages.
|
||||
pub struct LSPS0ClientHandler<ES: Deref>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
entropy_source: ES,
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
pending_events: Arc<EventQueue>,
|
||||
}
|
||||
|
||||
impl<ES: Deref> LSPS0ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
/// Returns a new instance of [`LSPS0ClientHandler`].
|
||||
pub(crate) fn new(
|
||||
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
|
||||
) -> Self {
|
||||
Self { entropy_source, pending_messages, pending_events }
|
||||
}
|
||||
|
||||
/// Calls LSPS0's `list_protocols`.
|
||||
///
|
||||
/// Please refer to the [LSPS0
|
||||
/// specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query)
|
||||
/// for more information.
|
||||
pub fn list_protocols(&self, counterparty_node_id: &PublicKey) {
|
||||
let msg = LSPS0Message::Request(
|
||||
utils::generate_request_id(&self.entropy_source),
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {}),
|
||||
);
|
||||
|
||||
self.pending_messages.enqueue(counterparty_node_id, msg.into());
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self, response: LSPS0Response, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match response {
|
||||
LSPS0Response::ListProtocols(ListProtocolsResponse { protocols }) => {
|
||||
self.pending_events.enqueue(Event::LSPS0Client(
|
||||
LSPS0ClientEvent::ListProtocolsResponse {
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
protocols,
|
||||
},
|
||||
));
|
||||
Ok(())
|
||||
},
|
||||
LSPS0Response::ListProtocolsError(ResponseError { code, message, data, .. }) => {
|
||||
Err(LightningError {
|
||||
err: format!(
|
||||
"ListProtocols error received. code = {}, message = {}, data = {:?}",
|
||||
code, message, data
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref> ProtocolMessageHandler for LSPS0ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
type ProtocolMessage = LSPS0Message;
|
||||
const PROTOCOL_NUMBER: Option<u16> = None;
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match message {
|
||||
LSPS0Message::Response(_, response) => {
|
||||
self.handle_response(response, counterparty_node_id)
|
||||
},
|
||||
LSPS0Message::Request(..) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Client handler received LSPS0 request message. This should never happen."
|
||||
);
|
||||
Err(LightningError { err: format!("Client handler received LSPS0 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use alloc::string::ToString;
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::lsps0::ser::{LSPSMessage, RequestId};
|
||||
use crate::tests::utils::{self, TestEntropy};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_list_protocols() {
|
||||
let pending_messages = Arc::new(MessageQueue::new());
|
||||
let entropy_source = Arc::new(TestEntropy {});
|
||||
let event_queue = Arc::new(EventQueue::new());
|
||||
|
||||
let lsps0_handler = Arc::new(LSPS0ClientHandler::new(
|
||||
entropy_source,
|
||||
Arc::clone(&pending_messages),
|
||||
event_queue,
|
||||
));
|
||||
|
||||
let counterparty_node_id = utils::parse_pubkey(
|
||||
"027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lsps0_handler.list_protocols(&counterparty_node_id);
|
||||
let pending_messages = pending_messages.get_and_clear_pending_msgs();
|
||||
|
||||
assert_eq!(pending_messages.len(), 1);
|
||||
|
||||
let (pubkey, message) = &pending_messages[0];
|
||||
|
||||
assert_eq!(*pubkey, counterparty_node_id);
|
||||
assert_eq!(
|
||||
*message,
|
||||
LSPSMessage::LSPS0(LSPS0Message::Request(
|
||||
RequestId("00000000000000000000000000000000".to_string()),
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {})
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
25
lightning-liquidity/src/lsps0/event.rs
Normal file
25
lightning-liquidity/src/lsps0/event.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains LSPS0 event types
|
||||
|
||||
use crate::prelude::Vec;
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
/// An event which an LSPS0 client may want to take some action in response to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS0ClientEvent {
|
||||
/// Information from the LSP about the protocols they support.
|
||||
ListProtocolsResponse {
|
||||
/// The node id of the LSP.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// A list of supported protocols.
|
||||
protocols: Vec<u16>,
|
||||
},
|
||||
}
|
16
lightning-liquidity/src/lsps0/mod.rs
Normal file
16
lightning-liquidity/src/lsps0/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Types and primitives that implement the LSPS0: Transport Layer specification.
|
||||
|
||||
pub mod client;
|
||||
pub mod event;
|
||||
pub mod msgs;
|
||||
pub mod ser;
|
||||
pub mod service;
|
218
lightning-liquidity/src/lsps0/msgs.rs
Normal file
218
lightning-liquidity/src/lsps0/msgs.rs
Normal file
|
@ -0,0 +1,218 @@
|
|||
//! Message, request, and other primitive types used to implement LSPS1.
|
||||
|
||||
use crate::lsps0::ser::{LSPSMessage, RequestId, ResponseError};
|
||||
use crate::prelude::Vec;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use core::convert::TryFrom;
|
||||
|
||||
pub(crate) const LSPS0_LISTPROTOCOLS_METHOD_NAME: &str = "lsps0.list_protocols";
|
||||
|
||||
/// A `list_protocols` request.
|
||||
///
|
||||
/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
pub struct ListProtocolsRequest {}
|
||||
|
||||
/// A response to a `list_protocols` request.
|
||||
///
|
||||
/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ListProtocolsResponse {
|
||||
/// A list of supported protocols.
|
||||
pub protocols: Vec<u16>,
|
||||
}
|
||||
|
||||
/// An LSPS0 protocol request.
|
||||
///
|
||||
/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS0Request {
|
||||
/// A request calling `list_protocols`.
|
||||
ListProtocols(ListProtocolsRequest),
|
||||
}
|
||||
|
||||
impl LSPS0Request {
|
||||
/// Returns the method name associated with the given request variant.
|
||||
pub fn method(&self) -> &str {
|
||||
match self {
|
||||
LSPS0Request::ListProtocols(_) => LSPS0_LISTPROTOCOLS_METHOD_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An LSPS0 protocol request.
|
||||
///
|
||||
/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS0Response {
|
||||
/// A response to a `list_protocols` request.
|
||||
ListProtocols(ListProtocolsResponse),
|
||||
/// An error response to a `list_protocols` request.
|
||||
ListProtocolsError(ResponseError),
|
||||
}
|
||||
|
||||
/// An LSPS0 protocol message.
|
||||
///
|
||||
/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS0Message {
|
||||
/// A request variant.
|
||||
Request(RequestId, LSPS0Request),
|
||||
/// A response variant.
|
||||
Response(RequestId, LSPS0Response),
|
||||
}
|
||||
|
||||
impl TryFrom<LSPSMessage> for LSPS0Message {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(message: LSPSMessage) -> Result<Self, Self::Error> {
|
||||
match message {
|
||||
LSPSMessage::Invalid(_) => Err(()),
|
||||
LSPSMessage::LSPS0(message) => Ok(message),
|
||||
LSPSMessage::LSPS1(_) => Err(()),
|
||||
LSPSMessage::LSPS2(_) => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LSPS0Message> for LSPSMessage {
|
||||
fn from(message: LSPS0Message) -> Self {
|
||||
LSPSMessage::LSPS0(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lightning::util::hash_tables::new_hash_map;
|
||||
|
||||
use super::*;
|
||||
use crate::lsps0::ser::LSPSMethod;
|
||||
use crate::prelude::ToString;
|
||||
|
||||
#[test]
|
||||
fn deserializes_request() {
|
||||
let json = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request:id:xyz123",
|
||||
"method": "lsps0.list_protocols"
|
||||
}"#;
|
||||
|
||||
let mut request_id_method_map = new_hash_map();
|
||||
|
||||
let msg = LSPSMessage::from_str_with_id_map(json, &mut request_id_method_map);
|
||||
assert!(msg.is_ok());
|
||||
let msg = msg.unwrap();
|
||||
assert_eq!(
|
||||
msg,
|
||||
LSPSMessage::LSPS0(LSPS0Message::Request(
|
||||
RequestId("request:id:xyz123".to_string()),
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_request() {
|
||||
let request = LSPSMessage::LSPS0(LSPS0Message::Request(
|
||||
RequestId("request:id:xyz123".to_string()),
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {}),
|
||||
));
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"jsonrpc":"2.0","id":"request:id:xyz123","method":"lsps0.list_protocols","params":{}}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_success_response() {
|
||||
let json = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request:id:xyz123",
|
||||
"result": {
|
||||
"protocols": [1,2,3]
|
||||
}
|
||||
}"#;
|
||||
let mut request_id_to_method_map = new_hash_map();
|
||||
request_id_to_method_map
|
||||
.insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols);
|
||||
|
||||
let response =
|
||||
LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
RequestId("request:id:xyz123".to_string()),
|
||||
LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![1, 2, 3] })
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_error_response() {
|
||||
let json = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request:id:xyz123",
|
||||
"error": {
|
||||
"code": -32617,
|
||||
"message": "Unknown Error"
|
||||
}
|
||||
}"#;
|
||||
let mut request_id_to_method_map = new_hash_map();
|
||||
request_id_to_method_map
|
||||
.insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols);
|
||||
|
||||
let response =
|
||||
LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
RequestId("request:id:xyz123".to_string()),
|
||||
LSPS0Response::ListProtocolsError(ResponseError {
|
||||
code: -32617,
|
||||
message: "Unknown Error".to_string(),
|
||||
data: None
|
||||
})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_fails_with_unknown_request_id() {
|
||||
let json = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request:id:xyz124",
|
||||
"result": {
|
||||
"protocols": [1,2,3]
|
||||
}
|
||||
}"#;
|
||||
let mut request_id_to_method_map = new_hash_map();
|
||||
request_id_to_method_map
|
||||
.insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols);
|
||||
|
||||
let response = LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map);
|
||||
assert!(response.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_response() {
|
||||
let response = LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
RequestId("request:id:xyz123".to_string()),
|
||||
LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![1, 2, 3] }),
|
||||
));
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"jsonrpc":"2.0","id":"request:id:xyz123","result":{"protocols":[1,2,3]}}"#
|
||||
);
|
||||
}
|
||||
}
|
714
lightning-liquidity/src/lsps0/ser.rs
Normal file
714
lightning-liquidity/src/lsps0/ser.rs
Normal file
|
@ -0,0 +1,714 @@
|
|||
//! Contains basic data types that allow for the (de-)seralization of LSPS messages in the JSON-RPC 2.0 format.
|
||||
//!
|
||||
//! Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more information.
|
||||
|
||||
use crate::lsps0::msgs::{
|
||||
LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsRequest,
|
||||
LSPS0_LISTPROTOCOLS_METHOD_NAME,
|
||||
};
|
||||
|
||||
use crate::lsps1::msgs::{
|
||||
LSPS1Message, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_METHOD_NAME,
|
||||
LSPS1_GET_INFO_METHOD_NAME, LSPS1_GET_ORDER_METHOD_NAME,
|
||||
};
|
||||
use crate::lsps2::msgs::{
|
||||
LSPS2Message, LSPS2Request, LSPS2Response, LSPS2_BUY_METHOD_NAME, LSPS2_GET_INFO_METHOD_NAME,
|
||||
};
|
||||
use crate::prelude::{HashMap, String};
|
||||
|
||||
use lightning::ln::msgs::LightningError;
|
||||
use lightning::ln::wire;
|
||||
use lightning::util::ser::WithoutLength;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
pub(crate) const LSPS_MESSAGE_SERIALIZED_STRUCT_NAME: &str = "LSPSMessage";
|
||||
pub(crate) const JSONRPC_FIELD_KEY: &str = "jsonrpc";
|
||||
pub(crate) const JSONRPC_FIELD_VALUE: &str = "2.0";
|
||||
pub(crate) const JSONRPC_METHOD_FIELD_KEY: &str = "method";
|
||||
pub(crate) const JSONRPC_ID_FIELD_KEY: &str = "id";
|
||||
pub(crate) const JSONRPC_PARAMS_FIELD_KEY: &str = "params";
|
||||
pub(crate) const JSONRPC_RESULT_FIELD_KEY: &str = "result";
|
||||
pub(crate) const JSONRPC_ERROR_FIELD_KEY: &str = "error";
|
||||
pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_CODE: i32 = -32700;
|
||||
pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE: &str = "parse error";
|
||||
pub(crate) const JSONRPC_INTERNAL_ERROR_ERROR_CODE: i32 = -32603;
|
||||
pub(crate) const JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE: &str = "Internal error";
|
||||
|
||||
pub(crate) const LSPS0_CLIENT_REJECTED_ERROR_CODE: i32 = 1;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum LSPSMethod {
|
||||
LSPS0ListProtocols,
|
||||
LSPS1GetInfo,
|
||||
LSPS1GetOrder,
|
||||
LSPS1CreateOrder,
|
||||
LSPS2GetInfo,
|
||||
LSPS2Buy,
|
||||
}
|
||||
|
||||
impl LSPSMethod {
|
||||
fn as_static_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::LSPS0ListProtocols => LSPS0_LISTPROTOCOLS_METHOD_NAME,
|
||||
Self::LSPS1GetInfo => LSPS1_GET_INFO_METHOD_NAME,
|
||||
Self::LSPS1CreateOrder => LSPS1_CREATE_ORDER_METHOD_NAME,
|
||||
Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME,
|
||||
Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME,
|
||||
Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LSPSMethod {
|
||||
type Err = &'static str;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
LSPS0_LISTPROTOCOLS_METHOD_NAME => Ok(Self::LSPS0ListProtocols),
|
||||
LSPS1_GET_INFO_METHOD_NAME => Ok(Self::LSPS1GetInfo),
|
||||
LSPS1_CREATE_ORDER_METHOD_NAME => Ok(Self::LSPS1CreateOrder),
|
||||
LSPS1_GET_ORDER_METHOD_NAME => Ok(Self::LSPS1GetOrder),
|
||||
LSPS2_GET_INFO_METHOD_NAME => Ok(Self::LSPS2GetInfo),
|
||||
LSPS2_BUY_METHOD_NAME => Ok(Self::LSPS2Buy),
|
||||
_ => Err(&"Unknown method name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LSPS0Request> for LSPSMethod {
|
||||
fn from(value: &LSPS0Request) -> Self {
|
||||
match value {
|
||||
LSPS0Request::ListProtocols(_) => Self::LSPS0ListProtocols,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LSPS1Request> for LSPSMethod {
|
||||
fn from(value: &LSPS1Request) -> Self {
|
||||
match value {
|
||||
LSPS1Request::GetInfo(_) => Self::LSPS1GetInfo,
|
||||
LSPS1Request::CreateOrder(_) => Self::LSPS1CreateOrder,
|
||||
LSPS1Request::GetOrder(_) => Self::LSPS1GetOrder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LSPS2Request> for LSPSMethod {
|
||||
fn from(value: &LSPS2Request) -> Self {
|
||||
match value {
|
||||
LSPS2Request::GetInfo(_) => Self::LSPS2GetInfo,
|
||||
LSPS2Request::Buy(_) => Self::LSPS2Buy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LSPSMethod {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <&str>::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LSPSMethod {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.as_static_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// The Lightning message type id for LSPS messages.
|
||||
pub const LSPS_MESSAGE_TYPE_ID: u16 = 37913;
|
||||
|
||||
/// A trait used to implement a specific LSPS protocol.
|
||||
///
|
||||
/// The messages the protocol uses need to be able to be mapped
|
||||
/// from and into [`LSPSMessage`].
|
||||
pub(crate) trait ProtocolMessageHandler {
|
||||
type ProtocolMessage: TryFrom<LSPSMessage> + Into<LSPSMessage>;
|
||||
const PROTOCOL_NUMBER: Option<u16>;
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError>;
|
||||
}
|
||||
|
||||
/// Lightning message type used by LSPS protocols.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RawLSPSMessage {
|
||||
/// The raw string payload that holds the actual message.
|
||||
pub payload: String,
|
||||
}
|
||||
|
||||
// We encode `RawLSPSMessage`'s payload without a length prefix as LSPS0 expects it to be the
|
||||
// remainder of the object.
|
||||
impl lightning::util::ser::Writeable for RawLSPSMessage {
|
||||
fn write<W: lightning::util::ser::Writer>(
|
||||
&self, w: &mut W,
|
||||
) -> Result<(), lightning::io::Error> {
|
||||
WithoutLength(&self.payload).write(w)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl lightning::util::ser::Readable for RawLSPSMessage {
|
||||
fn read<R: lightning::io::Read>(r: &mut R) -> Result<Self, lightning::ln::msgs::DecodeError> {
|
||||
let payload_without_length = WithoutLength::read(r)?;
|
||||
Ok(Self { payload: payload_without_length.0 })
|
||||
}
|
||||
}
|
||||
|
||||
impl wire::Type for RawLSPSMessage {
|
||||
fn type_id(&self) -> u16 {
|
||||
LSPS_MESSAGE_TYPE_ID
|
||||
}
|
||||
}
|
||||
|
||||
/// A JSON-RPC request's `id`.
|
||||
///
|
||||
/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#request_object) for
|
||||
/// more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct RequestId(pub String);
|
||||
|
||||
/// An error returned in response to an JSON-RPC request.
|
||||
///
|
||||
/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#error_object) for
|
||||
/// more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ResponseError {
|
||||
/// A number that indicates the error type that occurred.
|
||||
pub code: i32,
|
||||
/// A string providing a short description of the error.
|
||||
pub message: String,
|
||||
/// A primitive or structured value that contains additional information about the error.
|
||||
pub data: Option<String>,
|
||||
}
|
||||
|
||||
/// A (de-)serializable LSPS message allowing to be sent over the wire.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPSMessage {
|
||||
/// An invalid variant.
|
||||
Invalid(ResponseError),
|
||||
/// An LSPS0 message.
|
||||
LSPS0(LSPS0Message),
|
||||
/// An LSPS1 message.
|
||||
LSPS1(LSPS1Message),
|
||||
/// An LSPS2 message.
|
||||
LSPS2(LSPS2Message),
|
||||
}
|
||||
|
||||
impl LSPSMessage {
|
||||
/// A constructor returning an `LSPSMessage` from a raw JSON string.
|
||||
///
|
||||
/// The given `request_id_to_method` associates request ids with method names, as response objects
|
||||
/// don't carry the latter.
|
||||
pub(crate) fn from_str_with_id_map(
|
||||
json_str: &str, request_id_to_method_map: &mut HashMap<RequestId, LSPSMethod>,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
let deserializer = &mut serde_json::Deserializer::from_str(json_str);
|
||||
let visitor = LSPSMessageVisitor { request_id_to_method_map };
|
||||
deserializer.deserialize_any(visitor)
|
||||
}
|
||||
|
||||
/// Returns the request id and the method.
|
||||
pub(crate) fn get_request_id_and_method(&self) -> Option<(RequestId, LSPSMethod)> {
|
||||
match self {
|
||||
LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => {
|
||||
Some((RequestId(request_id.0.clone()), request.into()))
|
||||
},
|
||||
LSPSMessage::LSPS1(LSPS1Message::Request(request_id, request)) => {
|
||||
Some((RequestId(request_id.0.clone()), request.into()))
|
||||
},
|
||||
LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => {
|
||||
Some((RequestId(request_id.0.clone()), request.into()))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LSPSMessage {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut jsonrpc_object =
|
||||
serializer.serialize_struct(LSPS_MESSAGE_SERIALIZED_STRUCT_NAME, 3)?;
|
||||
|
||||
jsonrpc_object.serialize_field(JSONRPC_FIELD_KEY, JSONRPC_FIELD_VALUE)?;
|
||||
|
||||
match self {
|
||||
LSPSMessage::LSPS0(LSPS0Message::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 {
|
||||
LSPS0Request::ListProtocols(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
};
|
||||
},
|
||||
LSPSMessage::LSPS0(LSPS0Message::Response(request_id, response)) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?;
|
||||
|
||||
match response {
|
||||
LSPS0Response::ListProtocols(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?;
|
||||
},
|
||||
LSPS0Response::ListProtocolsError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?;
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS1(LSPS1Message::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 {
|
||||
LSPS1Request::GetInfo(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
LSPS1Request::CreateOrder(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
LSPS1Request::GetOrder(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS1(LSPS1Message::Response(request_id, response)) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?;
|
||||
|
||||
match response {
|
||||
LSPS1Response::GetInfo(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
|
||||
},
|
||||
LSPS1Response::GetInfoError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
|
||||
},
|
||||
LSPS1Response::CreateOrder(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
|
||||
},
|
||||
LSPS1Response::CreateOrderError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
|
||||
},
|
||||
LSPS1Response::GetOrder(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
|
||||
},
|
||||
LSPS1Response::GetOrderError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS2(LSPS2Message::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 {
|
||||
LSPS2Request::GetInfo(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
LSPS2Request::Buy(params) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)?
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS2(LSPS2Message::Response(request_id, response)) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?;
|
||||
|
||||
match response {
|
||||
LSPS2Response::GetInfo(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
|
||||
},
|
||||
LSPS2Response::GetInfoError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
|
||||
},
|
||||
LSPS2Response::Buy(result) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?
|
||||
},
|
||||
LSPS2Response::BuyError(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::Invalid(error) => {
|
||||
jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &serde_json::Value::Null)?;
|
||||
jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, &error)?;
|
||||
},
|
||||
}
|
||||
|
||||
jsonrpc_object.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct LSPSMessageVisitor<'a> {
|
||||
request_id_to_method_map: &'a mut HashMap<RequestId, LSPSMethod>,
|
||||
}
|
||||
|
||||
impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> {
|
||||
type Value = LSPSMessage;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("JSON-RPC object")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut id: Option<RequestId> = None;
|
||||
let mut method: Option<LSPSMethod> = None;
|
||||
let mut params = None;
|
||||
let mut result = None;
|
||||
let mut error: Option<ResponseError> = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"id" => {
|
||||
id = map.next_value()?;
|
||||
},
|
||||
"method" => {
|
||||
method = Some(map.next_value()?);
|
||||
},
|
||||
"params" => {
|
||||
params = Some(map.next_value()?);
|
||||
},
|
||||
"result" => {
|
||||
result = Some(map.next_value()?);
|
||||
},
|
||||
"error" => {
|
||||
error = Some(map.next_value()?);
|
||||
},
|
||||
_ => {
|
||||
let _: serde_json::Value = map.next_value()?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let id = match id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
if let Some(method) = method {
|
||||
return Err(de::Error::custom(format!(
|
||||
"Received unknown notification: {}",
|
||||
method.as_static_str()
|
||||
)));
|
||||
} else {
|
||||
if let Some(error) = error {
|
||||
if error.code == JSONRPC_INVALID_MESSAGE_ERROR_CODE {
|
||||
return Ok(LSPSMessage::Invalid(error));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(de::Error::custom("Received unknown error message"));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
match method {
|
||||
Some(method) => match method {
|
||||
LSPSMethod::LSPS0ListProtocols => Ok(LSPSMessage::LSPS0(LSPS0Message::Request(
|
||||
id,
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {}),
|
||||
))),
|
||||
LSPSMethod::LSPS1GetInfo => {
|
||||
let request = serde_json::from_value(params.unwrap_or(json!({})))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Request(
|
||||
id,
|
||||
LSPS1Request::GetInfo(request),
|
||||
)))
|
||||
},
|
||||
LSPSMethod::LSPS1CreateOrder => {
|
||||
let request = serde_json::from_value(params.unwrap_or(json!({})))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Request(
|
||||
id,
|
||||
LSPS1Request::CreateOrder(request),
|
||||
)))
|
||||
},
|
||||
LSPSMethod::LSPS1GetOrder => {
|
||||
let request = serde_json::from_value(params.unwrap_or(json!({})))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Request(
|
||||
id,
|
||||
LSPS1Request::GetOrder(request),
|
||||
)))
|
||||
},
|
||||
LSPSMethod::LSPS2GetInfo => {
|
||||
let request = serde_json::from_value(params.unwrap_or(json!({})))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Request(
|
||||
id,
|
||||
LSPS2Request::GetInfo(request),
|
||||
)))
|
||||
},
|
||||
LSPSMethod::LSPS2Buy => {
|
||||
let request = serde_json::from_value(params.unwrap_or(json!({})))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Request(id, LSPS2Request::Buy(request))))
|
||||
},
|
||||
},
|
||||
None => match self.request_id_to_method_map.remove(&id) {
|
||||
Some(method) => match method {
|
||||
LSPSMethod::LSPS0ListProtocols => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
id,
|
||||
LSPS0Response::ListProtocolsError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let list_protocols_response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
id,
|
||||
LSPS0Response::ListProtocols(list_protocols_response),
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
|
||||
}
|
||||
},
|
||||
LSPSMethod::LSPS1GetInfo => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::GetInfoError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::GetInfo(response),
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
|
||||
}
|
||||
},
|
||||
LSPSMethod::LSPS1CreateOrder => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::CreateOrderError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::CreateOrder(response),
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
|
||||
}
|
||||
},
|
||||
LSPSMethod::LSPS1GetOrder => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::GetOrderError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS1(LSPS1Message::Response(
|
||||
id,
|
||||
LSPS1Response::GetOrder(response),
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
|
||||
}
|
||||
},
|
||||
LSPSMethod::LSPS2GetInfo => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Response(
|
||||
id,
|
||||
LSPS2Response::GetInfoError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Response(
|
||||
id,
|
||||
LSPS2Response::GetInfo(response),
|
||||
)))
|
||||
} else {
|
||||
Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required"))
|
||||
}
|
||||
},
|
||||
LSPSMethod::LSPS2Buy => {
|
||||
if let Some(error) = error {
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Response(
|
||||
id,
|
||||
LSPS2Response::BuyError(error),
|
||||
)))
|
||||
} else if let Some(result) = result {
|
||||
let response =
|
||||
serde_json::from_value(result).map_err(de::Error::custom)?;
|
||||
Ok(LSPSMessage::LSPS2(LSPS2Message::Response(
|
||||
id,
|
||||
LSPS2Response::Buy(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: {}",
|
||||
id.0
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod string_amount {
|
||||
use crate::prelude::{String, ToString};
|
||||
use core::str::FromStr;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub(crate) fn serialize<S>(x: &u64, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_str(&x.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
|
||||
u64::from_str(&buf).map_err(|_| {
|
||||
serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid u64 amount string")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod string_amount_option {
|
||||
use crate::prelude::{String, ToString};
|
||||
use core::str::FromStr;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub(crate) fn serialize<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let v = x.as_ref().map(|v| v.to_string());
|
||||
Option::<String>::serialize(&v, s)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(buf) = Option::<String>::deserialize(deserializer)? {
|
||||
let val = u64::from_str(&buf).map_err(|_| {
|
||||
serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid u64 amount string")
|
||||
})?;
|
||||
Ok(Some(val))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod unchecked_address {
|
||||
use crate::prelude::{String, ToString};
|
||||
use bitcoin::Address;
|
||||
use core::str::FromStr;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub(crate) fn serialize<S>(x: &Address, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_str(&x.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Address, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let buf = String::deserialize(deserializer)?;
|
||||
|
||||
let parsed_addr = Address::from_str(&buf).map_err(|_| {
|
||||
serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid address string")
|
||||
})?;
|
||||
Ok(parsed_addr.assume_checked())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod unchecked_address_option {
|
||||
use crate::prelude::{String, ToString};
|
||||
use bitcoin::Address;
|
||||
use core::str::FromStr;
|
||||
use serde::de::Unexpected;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub(crate) fn serialize<S>(x: &Option<Address>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let v = x.as_ref().map(|v| v.to_string());
|
||||
Option::<String>::serialize(&v, s)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<bitcoin::Address>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
if let Some(buf) = Option::<String>::deserialize(deserializer)? {
|
||||
let val = Address::from_str(&buf).map_err(|_| {
|
||||
serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid address string")
|
||||
})?;
|
||||
Ok(Some(val.assume_checked()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod u32_fee_rate {
|
||||
use bitcoin::FeeRate;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub(crate) fn serialize<S>(x: &FeeRate, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let fee_rate_sat_kwu = x.to_sat_per_kwu();
|
||||
s.serialize_u32(fee_rate_sat_kwu as u32)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<FeeRate, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let fee_rate_sat_kwu = u32::deserialize(deserializer)?;
|
||||
|
||||
Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_kwu as u64))
|
||||
}
|
||||
}
|
122
lightning-liquidity/src/lsps0/service.rs
Normal file
122
lightning-liquidity/src/lsps0/service.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains the main LSPS0 server-side object, [`LSPS0ServiceHandler`].
|
||||
//!
|
||||
//! Please refer to the [LSPS0
|
||||
//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more
|
||||
//! information.
|
||||
|
||||
use crate::lsps0::msgs::{LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsResponse};
|
||||
use crate::lsps0::ser::{ProtocolMessageHandler, RequestId};
|
||||
use crate::message_queue::MessageQueue;
|
||||
use crate::prelude::Vec;
|
||||
use crate::sync::Arc;
|
||||
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::util::logger::Level;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
/// The main server-side object allowing to send and receive LSPS0 messages.
|
||||
pub struct LSPS0ServiceHandler {
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
protocols: Vec<u16>,
|
||||
}
|
||||
|
||||
impl LSPS0ServiceHandler {
|
||||
/// Returns a new instance of [`LSPS0ServiceHandler`].
|
||||
pub(crate) fn new(protocols: Vec<u16>, pending_messages: Arc<MessageQueue>) -> Self {
|
||||
Self { protocols, pending_messages }
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
&self, request_id: RequestId, request: LSPS0Request, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), lightning::ln::msgs::LightningError> {
|
||||
match request {
|
||||
LSPS0Request::ListProtocols(_) => {
|
||||
let msg = LSPS0Message::Response(
|
||||
request_id,
|
||||
LSPS0Response::ListProtocols(ListProtocolsResponse {
|
||||
protocols: self.protocols.clone(),
|
||||
}),
|
||||
);
|
||||
self.pending_messages.enqueue(counterparty_node_id, msg.into());
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolMessageHandler for LSPS0ServiceHandler {
|
||||
type ProtocolMessage = LSPS0Message;
|
||||
const PROTOCOL_NUMBER: Option<u16> = None;
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match message {
|
||||
LSPS0Message::Request(request_id, request) => {
|
||||
self.handle_request(request_id, request, counterparty_node_id)
|
||||
},
|
||||
LSPS0Message::Response(..) => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Service handler received LSPS0 response message. This should never happen."
|
||||
);
|
||||
Err(LightningError { err: format!("Service handler received LSPS0 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::lsps0::msgs::ListProtocolsRequest;
|
||||
use crate::lsps0::ser::LSPSMessage;
|
||||
use crate::tests::utils;
|
||||
use alloc::string::ToString;
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handle_list_protocols_request() {
|
||||
let protocols: Vec<u16> = vec![];
|
||||
let pending_messages = Arc::new(MessageQueue::new());
|
||||
|
||||
let lsps0_handler = Arc::new(LSPS0ServiceHandler::new(protocols, pending_messages.clone()));
|
||||
|
||||
let list_protocols_request = LSPS0Message::Request(
|
||||
RequestId("xyz123".to_string()),
|
||||
LSPS0Request::ListProtocols(ListProtocolsRequest {}),
|
||||
);
|
||||
let counterparty_node_id = utils::parse_pubkey(
|
||||
"027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lsps0_handler.handle_message(list_protocols_request, &counterparty_node_id).unwrap();
|
||||
let pending_messages = pending_messages.get_and_clear_pending_msgs();
|
||||
|
||||
assert_eq!(pending_messages.len(), 1);
|
||||
|
||||
let (pubkey, message) = &pending_messages[0];
|
||||
|
||||
assert_eq!(*pubkey, counterparty_node_id);
|
||||
assert_eq!(
|
||||
*message,
|
||||
LSPSMessage::LSPS0(LSPS0Message::Response(
|
||||
RequestId("xyz123".to_string()),
|
||||
LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![] })
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
471
lightning-liquidity/src/lsps1/client.rs
Normal file
471
lightning-liquidity/src/lsps1/client.rs
Normal file
|
@ -0,0 +1,471 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains the main LSPS1 client object, [`LSPS1ClientHandler`].
|
||||
|
||||
use super::event::LSPS1ClientEvent;
|
||||
use super::msgs::{
|
||||
CreateOrderRequest, CreateOrderResponse, GetInfoRequest, GetInfoResponse, GetOrderRequest,
|
||||
LSPS1Message, LSPS1Request, LSPS1Response, OrderId, OrderParameters,
|
||||
};
|
||||
use crate::message_queue::MessageQueue;
|
||||
|
||||
use crate::events::{Event, EventQueue};
|
||||
use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError};
|
||||
use crate::prelude::{new_hash_map, HashMap, HashSet};
|
||||
use crate::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::sign::EntropySource;
|
||||
use lightning::util::logger::Level;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
use bitcoin::Address;
|
||||
|
||||
use core::ops::Deref;
|
||||
|
||||
/// Client-side configuration options for LSPS1 channel requests.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LSPS1ClientConfig {
|
||||
/// The maximally allowed channel fees.
|
||||
pub max_channel_fees_msat: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PeerState {
|
||||
pending_get_info_requests: HashSet<RequestId>,
|
||||
pending_create_order_requests: HashSet<RequestId>,
|
||||
pending_get_order_requests: HashSet<RequestId>,
|
||||
}
|
||||
|
||||
/// The main object allowing to send and receive LSPS1 messages.
|
||||
pub struct LSPS1ClientHandler<ES: Deref>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
entropy_source: ES,
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
pending_events: Arc<EventQueue>,
|
||||
per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>,
|
||||
_config: LSPS1ClientConfig,
|
||||
}
|
||||
|
||||
impl<ES: Deref> LSPS1ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
/// Constructs an `LSPS1ClientHandler`.
|
||||
pub(crate) fn new(
|
||||
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
|
||||
config: LSPS1ClientConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
entropy_source,
|
||||
pending_messages,
|
||||
pending_events,
|
||||
per_peer_state: RwLock::new(new_hash_map()),
|
||||
_config: config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the supported options from the LSP.
|
||||
///
|
||||
/// The user will receive the LSP's response via an [`SupportedOptionsReady`] event.
|
||||
///
|
||||
/// `counterparty_node_id` is the `node_id` of the LSP you would like to use.
|
||||
///
|
||||
/// Returns the used [`RequestId`], which will be returned via [`SupportedOptionsReady`].
|
||||
///
|
||||
/// [`SupportedOptionsReady`]: crate::lsps1::event::LSPS1ClientEvent::SupportedOptionsReady
|
||||
pub fn request_supported_options(&self, counterparty_node_id: PublicKey) -> RequestId {
|
||||
let request_id = crate::utils::generate_request_id(&self.entropy_source);
|
||||
{
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::default()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
peer_state_lock.pending_get_info_requests.insert(request_id.clone());
|
||||
}
|
||||
|
||||
let request = LSPS1Request::GetInfo(GetInfoRequest {});
|
||||
let msg = LSPS1Message::Request(request_id.clone(), request).into();
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
request_id
|
||||
}
|
||||
|
||||
fn handle_get_info_response(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_get_info_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info response for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(
|
||||
LSPS1ClientEvent::SupportedOptionsReady {
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
supported_options: result.options,
|
||||
request_id,
|
||||
},
|
||||
));
|
||||
Ok(())
|
||||
},
|
||||
None => Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info response from unknown peer: {:?}",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_get_info_error(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_get_info_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info error for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(
|
||||
LSPS1ClientEvent::SupportedOptionsRequestFailed {
|
||||
request_id: request_id.clone(),
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
error: error.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info error response for request {:?}: {:?}",
|
||||
request_id, error
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Error),
|
||||
})
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info error response from an unknown counterparty ({:?})",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Places an order with the connected LSP given its `counterparty_node_id`.
|
||||
///
|
||||
/// The client agrees to paying channel fees according to the provided parameters.
|
||||
pub fn create_order(
|
||||
&self, counterparty_node_id: &PublicKey, order: OrderParameters,
|
||||
refund_onchain_address: Option<Address>,
|
||||
) -> RequestId {
|
||||
let (request_id, request_msg) = {
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(*counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::default()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
let request_id = crate::utils::generate_request_id(&self.entropy_source);
|
||||
let request =
|
||||
LSPS1Request::CreateOrder(CreateOrderRequest { order, refund_onchain_address });
|
||||
let msg = LSPS1Message::Request(request_id.clone(), request).into();
|
||||
peer_state_lock.pending_create_order_requests.insert(request_id.clone());
|
||||
|
||||
(request_id, Some(msg))
|
||||
};
|
||||
|
||||
if let Some(msg) = request_msg {
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
}
|
||||
|
||||
request_id
|
||||
}
|
||||
|
||||
fn handle_create_order_response(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey,
|
||||
response: CreateOrderResponse,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_create_order_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received create_order response for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(LSPS1ClientEvent::OrderCreated {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
order_id: response.order_id,
|
||||
order: response.order,
|
||||
payment: response.payment,
|
||||
channel: response.channel,
|
||||
}));
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received create_order response from unknown peer: {}",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_create_order_error(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_create_order_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received create order error for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(
|
||||
LSPS1ClientEvent::OrderRequestFailed {
|
||||
request_id: request_id.clone(),
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
error: error.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
Err(LightningError {
|
||||
err: format!(
|
||||
"Received create_order error response for request {:?}: {:?}",
|
||||
request_id, error
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Error),
|
||||
})
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received error response for a create order request from an unknown counterparty ({:?})",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the status of a pending payment, i.e., whether a payment has been received by the LSP.
|
||||
///
|
||||
/// Upon success an [`LSPS1ClientEvent::OrderStatus`] event will be emitted.
|
||||
///
|
||||
/// [`LSPS1ClientEvent::OrderStatus`]: crate::lsps1::event::LSPS1ClientEvent::OrderStatus
|
||||
pub fn check_order_status(
|
||||
&self, counterparty_node_id: &PublicKey, order_id: OrderId,
|
||||
) -> RequestId {
|
||||
let (request_id, request_msg) = {
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(*counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::default()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
let request_id = crate::utils::generate_request_id(&self.entropy_source);
|
||||
peer_state_lock.pending_get_order_requests.insert(request_id.clone());
|
||||
|
||||
let request = LSPS1Request::GetOrder(GetOrderRequest { order_id: order_id.clone() });
|
||||
let msg = LSPS1Message::Request(request_id.clone(), request).into();
|
||||
|
||||
(request_id, Some(msg))
|
||||
};
|
||||
|
||||
if let Some(msg) = request_msg {
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
}
|
||||
|
||||
request_id
|
||||
}
|
||||
|
||||
fn handle_get_order_response(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey,
|
||||
response: CreateOrderResponse,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_get_order_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_order response for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(LSPS1ClientEvent::OrderStatus {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
order_id: response.order_id,
|
||||
order: response.order,
|
||||
payment: response.payment,
|
||||
channel: response.channel,
|
||||
}));
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_order response from unknown peer: {}",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_get_order_error(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state_lock.pending_get_order_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get order error for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Client(
|
||||
LSPS1ClientEvent::OrderRequestFailed {
|
||||
request_id: request_id.clone(),
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
error: error.clone(),
|
||||
},
|
||||
));
|
||||
|
||||
Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_order error response for request {:?}: {:?}",
|
||||
request_id, error
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Error),
|
||||
})
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received error response for a get order request from an unknown counterparty ({:?})",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Debug),
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref> ProtocolMessageHandler for LSPS1ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
type ProtocolMessage = LSPS1Message;
|
||||
const PROTOCOL_NUMBER: Option<u16> = Some(1);
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match message {
|
||||
LSPS1Message::Response(request_id, response) => match response {
|
||||
LSPS1Response::GetInfo(params) => {
|
||||
self.handle_get_info_response(request_id, counterparty_node_id, params)
|
||||
},
|
||||
LSPS1Response::GetInfoError(error) => {
|
||||
self.handle_get_info_error(request_id, counterparty_node_id, error)
|
||||
},
|
||||
LSPS1Response::CreateOrder(params) => {
|
||||
self.handle_create_order_response(request_id, counterparty_node_id, params)
|
||||
},
|
||||
LSPS1Response::CreateOrderError(error) => {
|
||||
self.handle_create_order_error(request_id, counterparty_node_id, error)
|
||||
},
|
||||
LSPS1Response::GetOrder(params) => {
|
||||
self.handle_get_order_response(request_id, counterparty_node_id, params)
|
||||
},
|
||||
LSPS1Response::GetOrderError(error) => {
|
||||
self.handle_get_order_error(request_id, counterparty_node_id, error)
|
||||
},
|
||||
},
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Client handler received LSPS1 request message. This should never happen."
|
||||
);
|
||||
Err(LightningError {
|
||||
err: format!(
|
||||
"Client handler received LSPS1 request message from node {:?}. This should never happen.",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Error),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
183
lightning-liquidity/src/lsps1/event.rs
Normal file
183
lightning-liquidity/src/lsps1/event.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains LSPS1 event types
|
||||
|
||||
use super::msgs::OrderId;
|
||||
use super::msgs::{ChannelInfo, LSPS1Options, OrderParameters, PaymentInfo};
|
||||
|
||||
use crate::lsps0::ser::{RequestId, ResponseError};
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
/// An event which an LSPS1 client should take some action in response to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS1ClientEvent {
|
||||
/// A request previously issued via [`LSPS1ClientHandler::request_supported_options`]
|
||||
/// succeeded as the LSP returned the options it supports.
|
||||
///
|
||||
/// You must check whether LSP supports the parameters the client wants and then call
|
||||
/// [`LSPS1ClientHandler::create_order`] to place an order.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options
|
||||
/// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order
|
||||
SupportedOptionsReady {
|
||||
/// The identifier of the issued LSPS1 `get_info` request, as returned by
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP that provided this response.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// All options supported by the LSP.
|
||||
supported_options: LSPS1Options,
|
||||
},
|
||||
/// A request previously issued via [`LSPS1ClientHandler::request_supported_options`]
|
||||
/// failed as the LSP returned an error response.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options
|
||||
SupportedOptionsRequestFailed {
|
||||
/// The identifier of the issued LSPS1 `get_info` request, as returned by
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP that provided this response.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The error that was returned.
|
||||
error: ResponseError,
|
||||
},
|
||||
/// Confirmation from the LSP about the order created by the client.
|
||||
///
|
||||
/// When the payment is confirmed, the LSP will open a channel to you
|
||||
/// with the below agreed upon parameters.
|
||||
///
|
||||
/// You must pay the invoice or onchain address if you want to continue and then
|
||||
/// call [`LSPS1ClientHandler::check_order_status`] with the order id
|
||||
/// to get information from LSP about progress of the order.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status
|
||||
OrderCreated {
|
||||
/// The identifier of the issued LSPS1 `create_order` request, as returned by
|
||||
/// [`LSPS1ClientHandler::create_order`]
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The id of the channel order.
|
||||
order_id: OrderId,
|
||||
/// The order created by client and approved by LSP.
|
||||
order: OrderParameters,
|
||||
/// The details regarding payment of the order
|
||||
payment: PaymentInfo,
|
||||
/// The details regarding state of the channel ordered.
|
||||
channel: Option<ChannelInfo>,
|
||||
},
|
||||
/// Information from the LSP about the status of a previously created order.
|
||||
///
|
||||
/// Will be emitted in response to calling [`LSPS1ClientHandler::check_order_status`].
|
||||
///
|
||||
/// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status
|
||||
OrderStatus {
|
||||
/// The identifier of the issued LSPS1 `get_order` request, as returned by
|
||||
/// [`LSPS1ClientHandler::check_order_status`]
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The id of the channel order.
|
||||
order_id: OrderId,
|
||||
/// The order created by client and approved by LSP.
|
||||
order: OrderParameters,
|
||||
/// The details regarding payment of the order
|
||||
payment: PaymentInfo,
|
||||
/// The details regarding state of the channel ordered.
|
||||
channel: Option<ChannelInfo>,
|
||||
},
|
||||
/// A request previously issued via [`LSPS1ClientHandler::create_order`] or [`LSPS1ClientHandler::check_order_status`].
|
||||
/// failed as the LSP returned an error response.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order
|
||||
/// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status
|
||||
OrderRequestFailed {
|
||||
/// The identifier of the issued LSPS1 `create_order` or `get_order` request, as returned by
|
||||
/// [`LSPS1ClientHandler::create_order`] or [`LSPS1ClientHandler::check_order_status`].
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order
|
||||
/// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The error that was returned.
|
||||
error: ResponseError,
|
||||
},
|
||||
}
|
||||
|
||||
/// An event which an LSPS1 server should take some action in response to.
|
||||
#[cfg(lsps1_service)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS1ServiceEvent {
|
||||
/// A client has selected the parameters to use from the supported options of the LSP
|
||||
/// and would like to open a channel with the given payment parameters.
|
||||
///
|
||||
/// You must call [`LSPS1ServiceHandler::send_payment_details`] to
|
||||
/// send order parameters including the details regarding the
|
||||
/// payment and order id for this order for the client.
|
||||
///
|
||||
/// [`LSPS1ServiceHandler::send_payment_details`]: crate::lsps1::service::LSPS1ServiceHandler::send_payment_details
|
||||
RequestForPaymentDetails {
|
||||
/// An identifier that must be passed to [`LSPS1ServiceHandler::send_payment_details`].
|
||||
///
|
||||
/// [`LSPS1ServiceHandler::send_payment_details`]: crate::lsps1::service::LSPS1ServiceHandler::send_payment_details
|
||||
request_id: RequestId,
|
||||
/// The node id of the client making the information request.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The order requested by the client.
|
||||
order: OrderParameters,
|
||||
},
|
||||
/// A request from client to check the status of the payment.
|
||||
///
|
||||
/// An event to poll for checking payment status either onchain or lightning.
|
||||
///
|
||||
/// You must call [`LSPS1ServiceHandler::update_order_status`] to update the client
|
||||
/// regarding the status of the payment and order.
|
||||
///
|
||||
/// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status
|
||||
CheckPaymentConfirmation {
|
||||
/// An identifier that must be passed to [`LSPS1ServiceHandler::update_order_status`].
|
||||
///
|
||||
/// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status
|
||||
request_id: RequestId,
|
||||
/// The node id of the client making the information request.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The order id of order with pending payment.
|
||||
order_id: OrderId,
|
||||
},
|
||||
/// If error is encountered, refund the amount if paid by the client.
|
||||
Refund {
|
||||
/// An identifier.
|
||||
request_id: RequestId,
|
||||
/// The node id of the client making the information request.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The order id of the refunded order.
|
||||
order_id: OrderId,
|
||||
},
|
||||
}
|
16
lightning-liquidity/src/lsps1/mod.rs
Normal file
16
lightning-liquidity/src/lsps1/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Types and primitives that implement the LSPS1: Channel Request specification.
|
||||
|
||||
pub mod client;
|
||||
pub mod event;
|
||||
pub mod msgs;
|
||||
#[cfg(lsps1_service)]
|
||||
pub mod service;
|
473
lightning-liquidity/src/lsps1/msgs.rs
Normal file
473
lightning-liquidity/src/lsps1/msgs.rs
Normal file
|
@ -0,0 +1,473 @@
|
|||
//! Message, request, and other primitive types used to implement LSPS1.
|
||||
|
||||
use crate::lsps0::ser::{
|
||||
string_amount, u32_fee_rate, unchecked_address, unchecked_address_option, LSPSMessage,
|
||||
RequestId, ResponseError,
|
||||
};
|
||||
|
||||
use crate::prelude::String;
|
||||
|
||||
use bitcoin::{Address, FeeRate, OutPoint};
|
||||
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use core::convert::TryFrom;
|
||||
|
||||
pub(crate) const LSPS1_GET_INFO_METHOD_NAME: &str = "lsps1.get_info";
|
||||
pub(crate) const LSPS1_CREATE_ORDER_METHOD_NAME: &str = "lsps1.create_order";
|
||||
pub(crate) const LSPS1_GET_ORDER_METHOD_NAME: &str = "lsps1.get_order";
|
||||
|
||||
pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -32602;
|
||||
#[cfg(lsps1_service)]
|
||||
pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100;
|
||||
|
||||
/// The identifier of an order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
|
||||
pub struct OrderId(pub String);
|
||||
|
||||
/// A request made to an LSP to retrieve the supported options.
|
||||
///
|
||||
/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#1-lsps1info)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GetInfoRequest {}
|
||||
|
||||
/// An object representing the supported protocol options.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct LSPS1Options {
|
||||
/// The smallest number of confirmations needed for the LSP to accept a channel as confirmed.
|
||||
pub min_required_channel_confirmations: u16,
|
||||
/// The smallest number of blocks in which the LSP can confirm the funding transaction.
|
||||
pub min_funding_confirms_within_blocks: u16,
|
||||
/// Indicates if the LSP supports zero reserve.
|
||||
pub supports_zero_channel_reserve: bool,
|
||||
/// The maximum number of blocks a channel can be leased for.
|
||||
pub max_channel_expiry_blocks: u32,
|
||||
/// The minimum number of satoshi that the client MUST request.
|
||||
#[serde(with = "string_amount")]
|
||||
pub min_initial_client_balance_sat: u64,
|
||||
/// The maximum number of satoshi that the client MUST request.
|
||||
#[serde(with = "string_amount")]
|
||||
pub max_initial_client_balance_sat: u64,
|
||||
/// The minimum number of satoshi that the LSP will provide to the channel.
|
||||
#[serde(with = "string_amount")]
|
||||
pub min_initial_lsp_balance_sat: u64,
|
||||
/// The maximum number of satoshi that the LSP will provide to the channel.
|
||||
#[serde(with = "string_amount")]
|
||||
pub max_initial_lsp_balance_sat: u64,
|
||||
/// The minimal channel size.
|
||||
#[serde(with = "string_amount")]
|
||||
pub min_channel_balance_sat: u64,
|
||||
/// The maximal channel size.
|
||||
#[serde(with = "string_amount")]
|
||||
pub max_channel_balance_sat: u64,
|
||||
}
|
||||
|
||||
/// A response to a [`GetInfoRequest`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct GetInfoResponse {
|
||||
/// All options supported by the LSP.
|
||||
#[serde(flatten)]
|
||||
pub options: LSPS1Options,
|
||||
}
|
||||
|
||||
/// A request made to an LSP to create an order.
|
||||
///
|
||||
/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#2-lsps1create_order)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct CreateOrderRequest {
|
||||
/// The order made.
|
||||
#[serde(flatten)]
|
||||
pub order: OrderParameters,
|
||||
/// The address where the LSP will send the funds if the order fails.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "unchecked_address_option")]
|
||||
pub refund_onchain_address: Option<Address>,
|
||||
}
|
||||
|
||||
/// An object representing an LSPS1 channel order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct OrderParameters {
|
||||
/// Indicates how many satoshi the LSP will provide on their side.
|
||||
#[serde(with = "string_amount")]
|
||||
pub lsp_balance_sat: u64,
|
||||
/// Indicates how many satoshi the client will provide on their side.
|
||||
///
|
||||
/// The client sends these funds to the LSP, who will push them back to the client upon opening
|
||||
/// the channel.
|
||||
#[serde(with = "string_amount")]
|
||||
pub client_balance_sat: u64,
|
||||
/// The number of confirmations the funding tx must have before the LSP sends `channel_ready`.
|
||||
pub required_channel_confirmations: u16,
|
||||
/// The maximum number of blocks the client wants to wait until the funding transaction is confirmed.
|
||||
pub funding_confirms_within_blocks: u16,
|
||||
/// Indicates how long the channel is leased for in block time.
|
||||
pub channel_expiry_blocks: u32,
|
||||
/// May contain arbitrary associated data like a coupon code or a authentication token.
|
||||
pub token: Option<String>,
|
||||
/// Indicates if the channel should be announced to the network.
|
||||
pub announce_channel: bool,
|
||||
}
|
||||
|
||||
/// A response to a [`CreateOrderRequest`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct CreateOrderResponse {
|
||||
/// The id of the channel order.
|
||||
pub order_id: OrderId,
|
||||
/// The parameters of channel order.
|
||||
#[serde(flatten)]
|
||||
pub order: OrderParameters,
|
||||
/// The datetime when the order was created
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
/// The current state of the order.
|
||||
pub order_state: OrderState,
|
||||
/// Contains details about how to pay for the order.
|
||||
pub payment: PaymentInfo,
|
||||
/// Contains information about the channel state.
|
||||
pub channel: Option<ChannelInfo>,
|
||||
}
|
||||
|
||||
/// An object representing the state of an order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum OrderState {
|
||||
/// The order has been created.
|
||||
Created,
|
||||
/// The LSP has opened the channel and published the funding transaction.
|
||||
Completed,
|
||||
/// The order failed.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Details regarding how to pay for an order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct PaymentInfo {
|
||||
/// A Lightning payment using BOLT 11.
|
||||
pub bolt11: Option<Bolt11PaymentInfo>,
|
||||
/// An onchain payment.
|
||||
pub onchain: Option<OnchainPaymentInfo>,
|
||||
}
|
||||
|
||||
/// A Lightning payment using BOLT 11.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Bolt11PaymentInfo {
|
||||
/// Indicates the current state of the payment.
|
||||
pub state: PaymentState,
|
||||
/// The datetime when the payment option expires.
|
||||
pub expires_at: chrono::DateTime<Utc>,
|
||||
/// The total fee the LSP will charge to open this channel in satoshi.
|
||||
#[serde(with = "string_amount")]
|
||||
pub fee_total_sat: u64,
|
||||
/// The amount the client needs to pay to have the requested channel openend.
|
||||
#[serde(with = "string_amount")]
|
||||
pub order_total_sat: u64,
|
||||
/// A BOLT11 invoice the client can pay to have to channel opened.
|
||||
pub invoice: Bolt11Invoice,
|
||||
}
|
||||
|
||||
/// An onchain payment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct OnchainPaymentInfo {
|
||||
/// Indicates the current state of the payment.
|
||||
pub state: PaymentState,
|
||||
/// The datetime when the payment option expires.
|
||||
pub expires_at: chrono::DateTime<Utc>,
|
||||
/// The total fee the LSP will charge to open this channel in satoshi.
|
||||
#[serde(with = "string_amount")]
|
||||
pub fee_total_sat: u64,
|
||||
/// The amount the client needs to pay to have the requested channel openend.
|
||||
#[serde(with = "string_amount")]
|
||||
pub order_total_sat: u64,
|
||||
/// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel
|
||||
/// opened.
|
||||
#[serde(with = "unchecked_address")]
|
||||
pub address: Address,
|
||||
/// The minimum number of block confirmations that are required for the on-chain payment to be
|
||||
/// considered confirmed.
|
||||
pub min_onchain_payment_confirmations: Option<u16>,
|
||||
/// The minimum fee rate for the on-chain payment in case the client wants the payment to be
|
||||
/// confirmed without a confirmation.
|
||||
#[serde(with = "u32_fee_rate")]
|
||||
pub min_fee_for_0conf: FeeRate,
|
||||
/// The address where the LSP will send the funds if the order fails.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "unchecked_address_option")]
|
||||
pub refund_onchain_address: Option<Address>,
|
||||
}
|
||||
|
||||
/// The state of a payment.
|
||||
///
|
||||
/// *Note*: Previously, the spec also knew a `CANCELLED` state for BOLT11 payments, which has since
|
||||
/// been deprecated and `REFUNDED` should be used instead.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaymentState {
|
||||
/// A payment is expected.
|
||||
ExpectPayment,
|
||||
/// A sufficient payment has been received.
|
||||
Paid,
|
||||
/// The payment has been refunded.
|
||||
#[serde(alias = "CANCELLED")]
|
||||
Refunded,
|
||||
}
|
||||
|
||||
/// Details regarding a detected on-chain payment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct OnchainPayment {
|
||||
/// The outpoint of the payment.
|
||||
pub outpoint: String,
|
||||
/// The amount of satoshi paid.
|
||||
#[serde(with = "string_amount")]
|
||||
pub sat: u64,
|
||||
/// Indicates if the LSP regards the transaction as sufficiently confirmed.
|
||||
pub confirmed: bool,
|
||||
}
|
||||
|
||||
/// Details regarding the state of an ordered channel.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ChannelInfo {
|
||||
/// The datetime when the funding transaction has been published.
|
||||
pub funded_at: chrono::DateTime<Utc>,
|
||||
/// The outpoint of the funding transaction.
|
||||
pub funding_outpoint: OutPoint,
|
||||
/// The earliest datetime when the channel may be closed by the LSP.
|
||||
pub expires_at: chrono::DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// A request made to an LSP to retrieve information about an previously made order.
|
||||
///
|
||||
/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#21-lsps1get_order)
|
||||
/// for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct GetOrderRequest {
|
||||
/// The id of the order.
|
||||
pub order_id: OrderId,
|
||||
}
|
||||
|
||||
/// An enum that captures all the valid JSON-RPC requests in the LSPS1 protocol.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS1Request {
|
||||
/// A request to learn about the options supported by the LSP.
|
||||
GetInfo(GetInfoRequest),
|
||||
/// A request to create a channel order.
|
||||
CreateOrder(CreateOrderRequest),
|
||||
/// A request to query a previously created channel order.
|
||||
GetOrder(GetOrderRequest),
|
||||
}
|
||||
|
||||
/// An enum that captures all the valid JSON-RPC responses in the LSPS1 protocol.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS1Response {
|
||||
/// A successful response to a [`GetInfoRequest`].
|
||||
GetInfo(GetInfoResponse),
|
||||
/// An error response to a [`GetInfoRequest`].
|
||||
GetInfoError(ResponseError),
|
||||
/// A successful response to a [`CreateOrderRequest`].
|
||||
CreateOrder(CreateOrderResponse),
|
||||
/// An error response to a [`CreateOrderRequest`].
|
||||
CreateOrderError(ResponseError),
|
||||
/// A successful response to a [`GetOrderRequest`].
|
||||
GetOrder(CreateOrderResponse),
|
||||
/// An error response to a [`GetOrderRequest`].
|
||||
GetOrderError(ResponseError),
|
||||
}
|
||||
|
||||
/// An enum that captures all valid JSON-RPC messages in the LSPS1 protocol.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS1Message {
|
||||
/// An LSPS1 JSON-RPC request.
|
||||
Request(RequestId, LSPS1Request),
|
||||
/// An LSPS1 JSON-RPC response.
|
||||
Response(RequestId, LSPS1Response),
|
||||
}
|
||||
|
||||
impl TryFrom<LSPSMessage> for LSPS1Message {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(message: LSPSMessage) -> Result<Self, Self::Error> {
|
||||
if let LSPSMessage::LSPS1(message) = message {
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LSPS1Message> for LSPSMessage {
|
||||
fn from(message: LSPS1Message) -> Self {
|
||||
LSPSMessage::LSPS1(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::alloc::string::ToString;
|
||||
|
||||
#[test]
|
||||
fn options_supported_serialization() {
|
||||
let min_required_channel_confirmations = 0;
|
||||
let min_funding_confirms_within_blocks = 6;
|
||||
let supports_zero_channel_reserve = true;
|
||||
let max_channel_expiry_blocks = 144;
|
||||
let min_initial_client_balance_sat = 10_000_000;
|
||||
let max_initial_client_balance_sat = 100_000_000;
|
||||
let min_initial_lsp_balance_sat = 100_000;
|
||||
let max_initial_lsp_balance_sat = 100_000_000;
|
||||
let min_channel_balance_sat = 100_000;
|
||||
let max_channel_balance_sat = 100_000_000;
|
||||
|
||||
let options_supported = LSPS1Options {
|
||||
min_required_channel_confirmations,
|
||||
min_funding_confirms_within_blocks,
|
||||
supports_zero_channel_reserve,
|
||||
max_channel_expiry_blocks,
|
||||
min_initial_client_balance_sat,
|
||||
max_initial_client_balance_sat,
|
||||
min_initial_lsp_balance_sat,
|
||||
max_initial_lsp_balance_sat,
|
||||
min_channel_balance_sat,
|
||||
max_channel_balance_sat,
|
||||
};
|
||||
|
||||
let json_str = r#"{"max_channel_balance_sat":"100000000","max_channel_expiry_blocks":144,"max_initial_client_balance_sat":"100000000","max_initial_lsp_balance_sat":"100000000","min_channel_balance_sat":"100000","min_funding_confirms_within_blocks":6,"min_initial_client_balance_sat":"10000000","min_initial_lsp_balance_sat":"100000","min_required_channel_confirmations":0,"supports_zero_channel_reserve":true}"#;
|
||||
|
||||
assert_eq!(json_str, serde_json::json!(options_supported).to_string());
|
||||
assert_eq!(options_supported, serde_json::from_str(json_str).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_spec_test_vectors() {
|
||||
// Here, we simply assert that we're able to parse all examples given in LSPS1.
|
||||
let json_str = r#"{}"#;
|
||||
let _get_info_request: GetInfoRequest = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"min_required_channel_confirmations": 0,
|
||||
"min_funding_confirms_within_blocks" : 6,
|
||||
"supports_zero_channel_reserve": true,
|
||||
"max_channel_expiry_blocks": 20160,
|
||||
"min_initial_client_balance_sat": "20000",
|
||||
"max_initial_client_balance_sat": "100000000",
|
||||
"min_initial_lsp_balance_sat": "0",
|
||||
"max_initial_lsp_balance_sat": "100000000",
|
||||
"min_channel_balance_sat": "50000",
|
||||
"max_channel_balance_sat": "100000000"
|
||||
}"#;
|
||||
let _get_info_response: GetInfoResponse = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"lsp_balance_sat": "5000000",
|
||||
"client_balance_sat": "2000000",
|
||||
"required_channel_confirmations" : 0,
|
||||
"funding_confirms_within_blocks": 6,
|
||||
"channel_expiry_blocks": 144,
|
||||
"token": "",
|
||||
"refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h",
|
||||
"announce_channel": true
|
||||
}"#;
|
||||
let _create_order_request: CreateOrderRequest = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"state" : "EXPECT_PAYMENT",
|
||||
"expires_at": "2025-01-01T00:00:00Z",
|
||||
"fee_total_sat": "8888",
|
||||
"order_total_sat": "200888",
|
||||
"invoice": "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05"
|
||||
}"#;
|
||||
let _bolt11_payment: Bolt11PaymentInfo = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"state": "EXPECT_PAYMENT",
|
||||
"expires_at": "2025-01-01T00:00:00Z",
|
||||
"fee_total_sat": "9999",
|
||||
"order_total_sat": "200999",
|
||||
"address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr",
|
||||
"min_onchain_payment_confirmations": 1,
|
||||
"min_fee_for_0conf": 253
|
||||
}"#;
|
||||
let _onchain_payment: OnchainPaymentInfo = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"bolt11": {
|
||||
"state" : "EXPECT_PAYMENT",
|
||||
"expires_at": "2025-01-01T00:00:00Z",
|
||||
"fee_total_sat": "8888",
|
||||
"order_total_sat": "200888",
|
||||
"invoice": "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05"
|
||||
},
|
||||
"onchain": {
|
||||
"state": "EXPECT_PAYMENT",
|
||||
"expires_at": "2025-01-01T00:00:00Z",
|
||||
"fee_total_sat": "9999",
|
||||
"order_total_sat": "200999",
|
||||
"address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr",
|
||||
"min_onchain_payment_confirmations": 1,
|
||||
"min_fee_for_0conf": 253
|
||||
}
|
||||
}"#;
|
||||
let _payment: PaymentInfo = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c",
|
||||
"lsp_balance_sat": "5000000",
|
||||
"client_balance_sat": "2000000",
|
||||
"required_channel_confirmations" : 0,
|
||||
"funding_confirms_within_blocks": 1,
|
||||
"channel_expiry_blocks": 12,
|
||||
"token": "",
|
||||
"created_at": "2012-04-23T18:25:43.511Z",
|
||||
"announce_channel": true,
|
||||
"order_state": "CREATED",
|
||||
"payment": {
|
||||
"bolt11": {
|
||||
"state": "EXPECT_PAYMENT",
|
||||
"expires_at": "2015-01-25T19:29:44.612Z",
|
||||
"fee_total_sat": "8888",
|
||||
"order_total_sat": "2008888",
|
||||
"invoice" : "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05"
|
||||
},
|
||||
"onchain": {
|
||||
"state": "EXPECT_PAYMENT",
|
||||
"expires_at": "2015-01-25T19:29:44.612Z",
|
||||
"fee_total_sat": "9999",
|
||||
"order_total_sat": "2009999",
|
||||
"address" : "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr",
|
||||
"min_fee_for_0conf": 253,
|
||||
"min_onchain_payment_confirmations": 0,
|
||||
"refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h"
|
||||
}
|
||||
},
|
||||
"channel": null
|
||||
}"#;
|
||||
let _create_order_response: CreateOrderResponse = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c"
|
||||
}"#;
|
||||
let _get_order_request: GetOrderRequest = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"funded_at": "2012-04-23T18:25:43.511Z",
|
||||
"funding_outpoint": "0301e0480b374b32851a9462db29dc19fe830a7f7d7a88b81612b9d42099c0ae:0",
|
||||
"expires_at": "2012-04-23T18:25:43.511Z"
|
||||
}"#;
|
||||
let _channel: ChannelInfo = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#""CANCELLED""#;
|
||||
let payment_state: PaymentState = serde_json::from_str(json_str).unwrap();
|
||||
assert_eq!(payment_state, PaymentState::Refunded);
|
||||
|
||||
let json_str = r#""REFUNDED""#;
|
||||
let payment_state: PaymentState = serde_json::from_str(json_str).unwrap();
|
||||
assert_eq!(payment_state, PaymentState::Refunded);
|
||||
}
|
||||
}
|
459
lightning-liquidity/src/lsps1/service.rs
Normal file
459
lightning-liquidity/src/lsps1/service.rs
Normal file
|
@ -0,0 +1,459 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains the main LSPS1 server object, [`LSPS1ServiceHandler`].
|
||||
|
||||
use super::event::LSPS1ServiceEvent;
|
||||
use super::msgs::{
|
||||
ChannelInfo, CreateOrderRequest, CreateOrderResponse, GetInfoResponse, GetOrderRequest,
|
||||
LSPS1Message, LSPS1Options, LSPS1Request, LSPS1Response, OrderId, OrderParameters, OrderState,
|
||||
PaymentInfo, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE,
|
||||
};
|
||||
use super::utils::is_valid;
|
||||
use crate::message_queue::MessageQueue;
|
||||
|
||||
use crate::events::{Event, EventQueue};
|
||||
use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError};
|
||||
use crate::prelude::{new_hash_map, HashMap, String, ToString};
|
||||
use crate::sync::{Arc, Mutex, RwLock};
|
||||
use crate::utils;
|
||||
|
||||
use lightning::chain::Filter;
|
||||
use lightning::ln::channelmanager::AChannelManager;
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::sign::EntropySource;
|
||||
use lightning::util::errors::APIError;
|
||||
use lightning::util::logger::Level;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
use chrono::Utc;
|
||||
use core::ops::Deref;
|
||||
|
||||
/// Server-side configuration options for LSPS1 channel requests.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LSPS1ServiceConfig {
|
||||
/// A token to be send with each channel request.
|
||||
pub token: Option<String>,
|
||||
/// The options supported by the LSP.
|
||||
pub supported_options: Option<LSPS1Options>,
|
||||
}
|
||||
|
||||
struct ChannelStateError(String);
|
||||
|
||||
impl From<ChannelStateError> for LightningError {
|
||||
fn from(value: ChannelStateError) -> Self {
|
||||
LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum OutboundRequestState {
|
||||
OrderCreated { order_id: OrderId },
|
||||
WaitingPayment { order_id: OrderId },
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl OutboundRequestState {
|
||||
fn awaiting_payment(&self) -> Result<Self, ChannelStateError> {
|
||||
match self {
|
||||
OutboundRequestState::OrderCreated { order_id } => {
|
||||
Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() })
|
||||
},
|
||||
state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OutboundLSPS1Config {
|
||||
order: OrderParameters,
|
||||
created_at: chrono::DateTime<Utc>,
|
||||
payment: PaymentInfo,
|
||||
}
|
||||
|
||||
struct OutboundCRChannel {
|
||||
state: OutboundRequestState,
|
||||
config: OutboundLSPS1Config,
|
||||
}
|
||||
|
||||
impl OutboundCRChannel {
|
||||
fn new(
|
||||
order: OrderParameters, created_at: chrono::DateTime<Utc>, order_id: OrderId,
|
||||
payment: PaymentInfo,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: OutboundRequestState::OrderCreated { order_id },
|
||||
config: OutboundLSPS1Config { order, created_at, payment },
|
||||
}
|
||||
}
|
||||
fn awaiting_payment(&mut self) -> Result<(), LightningError> {
|
||||
self.state = self.state.awaiting_payment()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_order_validity(&self, supported_options: &LSPS1Options) -> bool {
|
||||
let order = &self.config.order;
|
||||
|
||||
is_valid(order, supported_options)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PeerState {
|
||||
outbound_channels_by_order_id: HashMap<OrderId, OutboundCRChannel>,
|
||||
request_to_cid: HashMap<RequestId, u128>,
|
||||
pending_requests: HashMap<RequestId, LSPS1Request>,
|
||||
}
|
||||
|
||||
impl PeerState {
|
||||
fn insert_outbound_channel(&mut self, order_id: OrderId, channel: OutboundCRChannel) {
|
||||
self.outbound_channels_by_order_id.insert(order_id, channel);
|
||||
}
|
||||
|
||||
fn insert_request(&mut self, request_id: RequestId, channel_id: u128) {
|
||||
self.request_to_cid.insert(request_id, channel_id);
|
||||
}
|
||||
|
||||
fn remove_outbound_channel(&mut self, order_id: OrderId) {
|
||||
self.outbound_channels_by_order_id.remove(&order_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The main object allowing to send and receive LSPS1 messages.
|
||||
pub struct LSPS1ServiceHandler<ES: Deref, CM: Deref + Clone, C: Deref>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
entropy_source: ES,
|
||||
channel_manager: CM,
|
||||
chain_source: Option<C>,
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
pending_events: Arc<EventQueue>,
|
||||
per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>,
|
||||
config: LSPS1ServiceConfig,
|
||||
}
|
||||
|
||||
impl<ES: Deref, CM: Deref + Clone, C: Deref> LSPS1ServiceHandler<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
/// Constructs a `LSPS1ServiceHandler`.
|
||||
pub(crate) fn new(
|
||||
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
|
||||
channel_manager: CM, chain_source: Option<C>, config: LSPS1ServiceConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
entropy_source,
|
||||
channel_manager,
|
||||
chain_source,
|
||||
pending_messages,
|
||||
pending_events,
|
||||
per_peer_state: RwLock::new(new_hash_map()),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_get_info_request(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
let response = LSPS1Response::GetInfo(GetInfoResponse {
|
||||
options: self
|
||||
.config
|
||||
.supported_options
|
||||
.clone()
|
||||
.ok_or(LightningError {
|
||||
err: format!("Configuration for LSP server not set."),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
let msg = LSPS1Message::Response(request_id, response).into();
|
||||
self.pending_messages.enqueue(counterparty_node_id, msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_create_order_request(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, params: CreateOrderRequest,
|
||||
) -> Result<(), LightningError> {
|
||||
if !is_valid(¶ms.order, &self.config.supported_options.as_ref().unwrap()) {
|
||||
let response = LSPS1Response::CreateOrderError(ResponseError {
|
||||
code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE,
|
||||
message: format!("Order does not match options supported by LSP server"),
|
||||
data: Some(format!(
|
||||
"Supported options are {:?}",
|
||||
&self.config.supported_options.as_ref().unwrap()
|
||||
)),
|
||||
});
|
||||
let msg = LSPS1Message::Response(request_id, response).into();
|
||||
self.pending_messages.enqueue(counterparty_node_id, msg);
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Client order does not match any supported options: {:?}",
|
||||
params.order
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(*counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::default()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
peer_state_lock
|
||||
.pending_requests
|
||||
.insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone()));
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Service(
|
||||
LSPS1ServiceEvent::RequestForPaymentDetails {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
order: params.order,
|
||||
},
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Used by LSP to send response containing details regarding the channel fees and payment information.
|
||||
///
|
||||
/// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] event.
|
||||
///
|
||||
/// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails
|
||||
pub fn send_payment_details(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, payment: PaymentInfo,
|
||||
created_at: chrono::DateTime<Utc>,
|
||||
) -> Result<(), APIError> {
|
||||
let (result, response) = {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
match peer_state_lock.pending_requests.remove(&request_id) {
|
||||
Some(LSPS1Request::CreateOrder(params)) => {
|
||||
let order_id = self.generate_order_id();
|
||||
let channel = OutboundCRChannel::new(
|
||||
params.order.clone(),
|
||||
created_at.clone(),
|
||||
order_id.clone(),
|
||||
payment.clone(),
|
||||
);
|
||||
|
||||
peer_state_lock.insert_outbound_channel(order_id.clone(), channel);
|
||||
|
||||
let response = LSPS1Response::CreateOrder(CreateOrderResponse {
|
||||
order: params.order,
|
||||
order_id,
|
||||
order_state: OrderState::Created,
|
||||
created_at,
|
||||
payment,
|
||||
channel: None,
|
||||
});
|
||||
|
||||
(Ok(()), Some(response))
|
||||
},
|
||||
|
||||
_ => (
|
||||
Err(APIError::APIMisuseError {
|
||||
err: format!(
|
||||
"No pending buy request for request_id: {:?}",
|
||||
request_id
|
||||
),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
}
|
||||
},
|
||||
None => (
|
||||
Err(APIError::APIMisuseError {
|
||||
err: format!(
|
||||
"No state for the counterparty exists: {:?}",
|
||||
counterparty_node_id
|
||||
),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(response) = response {
|
||||
let msg = LSPS1Message::Response(request_id, response).into();
|
||||
self.pending_messages.enqueue(counterparty_node_id, msg);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn handle_get_order_request(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetOrderRequest,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(&counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
let outbound_channel = peer_state_lock
|
||||
.outbound_channels_by_order_id
|
||||
.get_mut(¶ms.order_id)
|
||||
.ok_or(LightningError {
|
||||
err: format!(
|
||||
"Received get order request for unknown order id {:?}",
|
||||
params.order_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})?;
|
||||
|
||||
if let Err(e) = outbound_channel.awaiting_payment() {
|
||||
peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id);
|
||||
self.pending_events.enqueue(Event::LSPS1Service(LSPS1ServiceEvent::Refund {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
order_id: params.order_id,
|
||||
}));
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
peer_state_lock
|
||||
.pending_requests
|
||||
.insert(request_id.clone(), LSPS1Request::GetOrder(params.clone()));
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS1Service(
|
||||
LSPS1ServiceEvent::CheckPaymentConfirmation {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
order_id: params.order_id,
|
||||
},
|
||||
));
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!("Received error response for a create order request from an unknown counterparty ({:?})", counterparty_node_id),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Used by LSP to give details to client regarding the status of channel opening.
|
||||
/// Called to respond to client's GetOrder request.
|
||||
/// The LSP continously polls for checking payment confirmation on-chain or lighting
|
||||
/// and then responds to client request.
|
||||
///
|
||||
/// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event.
|
||||
///
|
||||
/// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation
|
||||
pub fn update_order_status(
|
||||
&self, request_id: RequestId, counterparty_node_id: PublicKey, order_id: OrderId,
|
||||
order_state: OrderState, channel: Option<ChannelInfo>,
|
||||
) -> Result<(), APIError> {
|
||||
let (result, response) = {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
|
||||
match outer_state_lock.get(&counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
if let Some(outbound_channel) =
|
||||
peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id)
|
||||
{
|
||||
let config = &outbound_channel.config;
|
||||
|
||||
let response = LSPS1Response::GetOrder(CreateOrderResponse {
|
||||
order_id,
|
||||
order: config.order.clone(),
|
||||
order_state,
|
||||
created_at: config.created_at,
|
||||
payment: config.payment.clone(),
|
||||
channel,
|
||||
});
|
||||
(Ok(()), Some(response))
|
||||
} else {
|
||||
(
|
||||
Err(APIError::APIMisuseError {
|
||||
err: format!("Channel with order_id {} not found", order_id.0),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
}
|
||||
},
|
||||
None => (
|
||||
Err(APIError::APIMisuseError {
|
||||
err: format!(
|
||||
"No existing state with counterparty {}",
|
||||
counterparty_node_id
|
||||
),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(response) = response {
|
||||
let msg = LSPS1Message::Response(request_id, response).into();
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn generate_order_id(&self) -> OrderId {
|
||||
let bytes = self.entropy_source.get_secure_random_bytes();
|
||||
OrderId(utils::hex_str(&bytes[0..16]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref, CM: Deref + Clone, C: Deref> ProtocolMessageHandler
|
||||
for LSPS1ServiceHandler<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
type ProtocolMessage = LSPS1Message;
|
||||
const PROTOCOL_NUMBER: Option<u16> = Some(1);
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match message {
|
||||
LSPS1Message::Request(request_id, request) => match request {
|
||||
LSPS1Request::GetInfo(_) => {
|
||||
self.handle_get_info_request(request_id, counterparty_node_id)
|
||||
},
|
||||
LSPS1Request::CreateOrder(params) => {
|
||||
self.handle_create_order_request(request_id, counterparty_node_id, params)
|
||||
},
|
||||
LSPS1Request::GetOrder(params) => {
|
||||
self.handle_get_order_request(request_id, counterparty_node_id, params)
|
||||
},
|
||||
},
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Service handler received LSPS1 response message. This should never happen."
|
||||
);
|
||||
Err(LightningError { err: format!("Service handler received LSPS1 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
358
lightning-liquidity/src/lsps2/client.rs
Normal file
358
lightning-liquidity/src/lsps2/client.rs
Normal file
|
@ -0,0 +1,358 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains the main LSPS2 client object, [`LSPS2ClientHandler`].
|
||||
|
||||
use crate::events::{Event, EventQueue};
|
||||
use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError};
|
||||
use crate::lsps2::event::LSPS2ClientEvent;
|
||||
use crate::message_queue::MessageQueue;
|
||||
use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet, String, ToString};
|
||||
use crate::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::sign::EntropySource;
|
||||
use lightning::util::errors::APIError;
|
||||
use lightning::util::logger::Level;
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
use core::default::Default;
|
||||
use core::ops::Deref;
|
||||
|
||||
use crate::lsps2::msgs::{
|
||||
BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, LSPS2Message, LSPS2Request,
|
||||
LSPS2Response, OpeningFeeParams,
|
||||
};
|
||||
|
||||
/// Client-side configuration options for JIT channels.
|
||||
#[derive(Clone, Debug, Copy, Default)]
|
||||
pub struct LSPS2ClientConfig {}
|
||||
|
||||
struct InboundJITChannel {
|
||||
payment_size_msat: Option<u64>,
|
||||
}
|
||||
|
||||
impl InboundJITChannel {
|
||||
fn new(payment_size_msat: Option<u64>) -> Self {
|
||||
Self { payment_size_msat }
|
||||
}
|
||||
}
|
||||
|
||||
struct PeerState {
|
||||
pending_get_info_requests: HashSet<RequestId>,
|
||||
pending_buy_requests: HashMap<RequestId, InboundJITChannel>,
|
||||
}
|
||||
|
||||
impl PeerState {
|
||||
fn new() -> Self {
|
||||
let pending_get_info_requests = new_hash_set();
|
||||
let pending_buy_requests = new_hash_map();
|
||||
Self { pending_get_info_requests, pending_buy_requests }
|
||||
}
|
||||
}
|
||||
|
||||
/// The main object allowing to send and receive LSPS2 messages.
|
||||
///
|
||||
/// Note that currently only the 'client-trusts-LSP' trust model is supported, i.e., we don't
|
||||
/// provide any additional API guidance to allow withholding the preimage until the channel is
|
||||
/// opened. Please refer to the [`LSPS2 specification`] for more information.
|
||||
///
|
||||
/// [`LSPS2 specification`]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#trust-models
|
||||
pub struct LSPS2ClientHandler<ES: Deref>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
entropy_source: ES,
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
pending_events: Arc<EventQueue>,
|
||||
per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>,
|
||||
_config: LSPS2ClientConfig,
|
||||
}
|
||||
|
||||
impl<ES: Deref> LSPS2ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
/// Constructs an `LSPS2ClientHandler`.
|
||||
pub(crate) fn new(
|
||||
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
|
||||
_config: LSPS2ClientConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
entropy_source,
|
||||
pending_messages,
|
||||
pending_events,
|
||||
per_peer_state: RwLock::new(new_hash_map()),
|
||||
_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the channel opening parameters from the LSP.
|
||||
///
|
||||
/// This initiates the JIT-channel flow that, at the end of it, will have the LSP
|
||||
/// open a channel with sufficient inbound liquidity to be able to receive the payment.
|
||||
///
|
||||
/// The user will receive the LSP's response via an [`OpeningParametersReady`] event.
|
||||
///
|
||||
/// `counterparty_node_id` is the `node_id` of the LSP you would like to use.
|
||||
///
|
||||
/// `token` is an optional `String` that will be provided to the LSP.
|
||||
/// It can be used by the LSP as an API key, coupon code, or some other way to identify a user.
|
||||
///
|
||||
/// Returns the used [`RequestId`], which will be returned via [`OpeningParametersReady`].
|
||||
///
|
||||
/// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady
|
||||
pub fn request_opening_params(
|
||||
&self, counterparty_node_id: PublicKey, token: Option<String>,
|
||||
) -> RequestId {
|
||||
let request_id = crate::utils::generate_request_id(&self.entropy_source);
|
||||
|
||||
{
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::new()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
peer_state_lock.pending_get_info_requests.insert(request_id.clone());
|
||||
}
|
||||
|
||||
let request = LSPS2Request::GetInfo(GetInfoRequest { token });
|
||||
let msg = LSPS2Message::Request(request_id.clone(), request).into();
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
|
||||
request_id
|
||||
}
|
||||
|
||||
/// Confirms a set of chosen channel opening parameters to use for the JIT channel and
|
||||
/// requests the necessary invoice generation parameters from the LSP.
|
||||
///
|
||||
/// Should be called in response to receiving a [`OpeningParametersReady`] event.
|
||||
///
|
||||
/// The user will receive the LSP's response via an [`InvoiceParametersReady`] event.
|
||||
///
|
||||
/// If `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount
|
||||
/// and MPP can be used to pay it.
|
||||
///
|
||||
/// If `payment_size_msat` is [`Option::None`] then the invoice can be for an arbitrary amount
|
||||
/// but MPP can no longer be used to pay it.
|
||||
///
|
||||
/// The client agrees to paying an opening fee equal to
|
||||
/// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`.
|
||||
///
|
||||
/// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady
|
||||
/// [`InvoiceParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::InvoiceParametersReady
|
||||
pub fn select_opening_params(
|
||||
&self, counterparty_node_id: PublicKey, payment_size_msat: Option<u64>,
|
||||
opening_fee_params: OpeningFeeParams,
|
||||
) -> Result<RequestId, APIError> {
|
||||
let request_id = crate::utils::generate_request_id(&self.entropy_source);
|
||||
|
||||
{
|
||||
let mut outer_state_lock = self.per_peer_state.write().unwrap();
|
||||
let inner_state_lock = outer_state_lock
|
||||
.entry(counterparty_node_id)
|
||||
.or_insert(Mutex::new(PeerState::new()));
|
||||
let mut peer_state_lock = inner_state_lock.lock().unwrap();
|
||||
|
||||
let jit_channel = InboundJITChannel::new(payment_size_msat);
|
||||
if peer_state_lock
|
||||
.pending_buy_requests
|
||||
.insert(request_id.clone(), jit_channel)
|
||||
.is_some()
|
||||
{
|
||||
return Err(APIError::APIMisuseError {
|
||||
err: "Failed due to duplicate request_id. This should never happen!"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let request = LSPS2Request::Buy(BuyRequest { opening_fee_params, payment_size_msat });
|
||||
let msg = LSPS2Message::Request(request_id.clone(), request).into();
|
||||
self.pending_messages.enqueue(&counterparty_node_id, msg);
|
||||
|
||||
Ok(request_id)
|
||||
}
|
||||
|
||||
fn handle_get_info_response(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state.pending_get_info_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info response for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
}
|
||||
|
||||
self.pending_events.enqueue(Event::LSPS2Client(
|
||||
LSPS2ClientEvent::OpeningParametersReady {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
opening_fee_params_menu: result.opening_fee_params_menu,
|
||||
},
|
||||
));
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info response from unknown peer: {:?}",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_get_info_error(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state = inner_state_lock.lock().unwrap();
|
||||
|
||||
if !peer_state.pending_get_info_requests.remove(&request_id) {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received get_info error for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buy_response(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, result: BuyResponse,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state = inner_state_lock.lock().unwrap();
|
||||
|
||||
let jit_channel =
|
||||
peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError {
|
||||
err: format!(
|
||||
"Received buy response for an unknown request: {:?}",
|
||||
request_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})?;
|
||||
|
||||
if let Ok(intercept_scid) = result.jit_channel_scid.to_scid() {
|
||||
self.pending_events.enqueue(Event::LSPS2Client(
|
||||
LSPS2ClientEvent::InvoiceParametersReady {
|
||||
request_id,
|
||||
counterparty_node_id: *counterparty_node_id,
|
||||
intercept_scid,
|
||||
cltv_expiry_delta: result.lsp_cltv_expiry_delta,
|
||||
payment_size_msat: jit_channel.payment_size_msat,
|
||||
},
|
||||
));
|
||||
} else {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received buy response with an invalid intercept scid {:?}",
|
||||
result.jit_channel_scid
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError {
|
||||
err: format!(
|
||||
"Received buy response from unknown peer: {:?}",
|
||||
counterparty_node_id
|
||||
),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
});
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_buy_error(
|
||||
&self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError,
|
||||
) -> Result<(), LightningError> {
|
||||
let outer_state_lock = self.per_peer_state.read().unwrap();
|
||||
match outer_state_lock.get(counterparty_node_id) {
|
||||
Some(inner_state_lock) => {
|
||||
let mut peer_state = inner_state_lock.lock().unwrap();
|
||||
|
||||
peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError {
|
||||
err: format!("Received buy error for an unknown request: {:?}", request_id),
|
||||
action: ErrorAction::IgnoreAndLog(Level::Info),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref> ProtocolMessageHandler for LSPS2ClientHandler<ES>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
type ProtocolMessage = LSPS2Message;
|
||||
const PROTOCOL_NUMBER: Option<u16> = Some(2);
|
||||
|
||||
fn handle_message(
|
||||
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
|
||||
) -> Result<(), LightningError> {
|
||||
match message {
|
||||
LSPS2Message::Response(request_id, response) => match response {
|
||||
LSPS2Response::GetInfo(result) => {
|
||||
self.handle_get_info_response(request_id, counterparty_node_id, result)
|
||||
},
|
||||
LSPS2Response::GetInfoError(error) => {
|
||||
self.handle_get_info_error(request_id, counterparty_node_id, error)
|
||||
},
|
||||
LSPS2Response::Buy(result) => {
|
||||
self.handle_buy_response(request_id, counterparty_node_id, result)
|
||||
},
|
||||
LSPS2Response::BuyError(error) => {
|
||||
self.handle_buy_error(request_id, counterparty_node_id, error)
|
||||
},
|
||||
},
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Client handler received LSPS2 request message. This should never happen."
|
||||
);
|
||||
Err(LightningError { err: format!("Client handler received LSPS2 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
125
lightning-liquidity/src/lsps2/event.rs
Normal file
125
lightning-liquidity/src/lsps2/event.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Contains LSPS2 event types
|
||||
|
||||
use super::msgs::OpeningFeeParams;
|
||||
use crate::lsps0::ser::RequestId;
|
||||
use crate::prelude::{String, Vec};
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
/// An event which an LSPS2 client should take some action in response to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS2ClientEvent {
|
||||
/// Information from the LSP about their current fee rates and channel parameters.
|
||||
///
|
||||
/// You must call [`LSPS2ClientHandler::select_opening_params`] with the fee parameter
|
||||
/// you want to use if you wish to proceed opening a channel.
|
||||
///
|
||||
/// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params
|
||||
OpeningParametersReady {
|
||||
/// The identifier of the issued LSPS2 `get_info` request, as returned by
|
||||
/// [`LSPS2ClientHandler::request_opening_params`]
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS2ClientHandler::request_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::request_opening_params
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP that provided this response.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The menu of fee parameters the LSP is offering at this time.
|
||||
/// You must select one of these if you wish to proceed.
|
||||
opening_fee_params_menu: Vec<OpeningFeeParams>,
|
||||
},
|
||||
/// Provides the necessary information to generate a payable invoice that then may be given to
|
||||
/// the payer.
|
||||
///
|
||||
/// When the invoice is paid, the LSP will open a channel with the previously agreed upon
|
||||
/// parameters to you.
|
||||
InvoiceParametersReady {
|
||||
/// The identifier of the issued LSPS2 `buy` request, as returned by
|
||||
/// [`LSPS2ClientHandler::select_opening_params`].
|
||||
///
|
||||
/// This can be used to track which request this event corresponds to.
|
||||
///
|
||||
/// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params
|
||||
request_id: RequestId,
|
||||
/// The node id of the LSP.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The intercept short channel id to use in the route hint.
|
||||
intercept_scid: u64,
|
||||
/// The `cltv_expiry_delta` to use in the route hint.
|
||||
cltv_expiry_delta: u32,
|
||||
/// The initial payment size you specified.
|
||||
payment_size_msat: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An event which an LSPS2 server should take some action in response to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LSPS2ServiceEvent {
|
||||
/// A request from a client for information about JIT Channel parameters.
|
||||
///
|
||||
/// You must calculate the parameters for this client and pass them to
|
||||
/// [`LSPS2ServiceHandler::opening_fee_params_generated`].
|
||||
///
|
||||
/// If an unrecognized or stale token is provided you can use
|
||||
/// `[LSPS2ServiceHandler::invalid_token_provided`] to error the request.
|
||||
///
|
||||
/// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated
|
||||
/// [`LSPS2ServiceHandler::invalid_token_provided`]: crate::lsps2::service::LSPS2ServiceHandler::invalid_token_provided
|
||||
GetInfo {
|
||||
/// An identifier that must be passed to [`LSPS2ServiceHandler::opening_fee_params_generated`].
|
||||
///
|
||||
/// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated
|
||||
request_id: RequestId,
|
||||
/// The node id of the client making the information request.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// An optional token that can be used as an API key, coupon code, etc.
|
||||
token: Option<String>,
|
||||
},
|
||||
/// A client has selected a opening fee parameter to use and would like to
|
||||
/// purchase a channel with an optional initial payment size.
|
||||
///
|
||||
/// If `payment_size_msat` is [`Option::Some`] then the payer is allowed to use MPP.
|
||||
/// If `payment_size_msat` is [`Option::None`] then the payer cannot use MPP.
|
||||
///
|
||||
/// You must generate an intercept scid and `cltv_expiry_delta` for them to use
|
||||
/// and call [`LSPS2ServiceHandler::invoice_parameters_generated`].
|
||||
///
|
||||
/// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated
|
||||
BuyRequest {
|
||||
/// An identifier that must be passed into [`LSPS2ServiceHandler::invoice_parameters_generated`].
|
||||
///
|
||||
/// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated
|
||||
request_id: RequestId,
|
||||
/// The client node id that is making this request.
|
||||
counterparty_node_id: PublicKey,
|
||||
/// The channel parameters they have selected.
|
||||
opening_fee_params: OpeningFeeParams,
|
||||
/// The size of the initial payment they would like to receive.
|
||||
payment_size_msat: Option<u64>,
|
||||
},
|
||||
/// You should open a channel using [`ChannelManager::create_channel`].
|
||||
///
|
||||
/// [`ChannelManager::create_channel`]: lightning::ln::channelmanager::ChannelManager::create_channel
|
||||
OpenChannel {
|
||||
/// The node to open channel with.
|
||||
their_network_key: PublicKey,
|
||||
/// The amount to forward after fees.
|
||||
amt_to_forward_msat: u64,
|
||||
/// The fee earned for opening the channel.
|
||||
opening_fee_msat: u64,
|
||||
/// A user specified id used to track channel open.
|
||||
user_channel_id: u128,
|
||||
/// The intercept short channel id to use in the route hint.
|
||||
intercept_scid: u64,
|
||||
},
|
||||
}
|
17
lightning-liquidity/src/lsps2/mod.rs
Normal file
17
lightning-liquidity/src/lsps2/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Implementation of LSPS2: JIT Channel Negotiation specification.
|
||||
|
||||
pub mod client;
|
||||
pub mod event;
|
||||
pub mod msgs;
|
||||
pub(crate) mod payment_queue;
|
||||
pub mod service;
|
||||
pub mod utils;
|
434
lightning-liquidity/src/lsps2/msgs.rs
Normal file
434
lightning-liquidity/src/lsps2/msgs.rs
Normal file
|
@ -0,0 +1,434 @@
|
|||
//! Message, request, and other primitive types used to implement LSPS2.
|
||||
|
||||
use core::convert::TryFrom;
|
||||
|
||||
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
|
||||
use bitcoin::hashes::sha256::Hash as Sha256;
|
||||
use bitcoin::hashes::{Hash, HashEngine};
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use lightning::util::scid_utils;
|
||||
|
||||
use crate::lsps0::ser::{
|
||||
string_amount, string_amount_option, LSPSMessage, RequestId, ResponseError,
|
||||
};
|
||||
use crate::prelude::{String, Vec};
|
||||
use crate::utils;
|
||||
|
||||
pub(crate) const LSPS2_GET_INFO_METHOD_NAME: &str = "lsps2.get_info";
|
||||
pub(crate) const LSPS2_BUY_METHOD_NAME: &str = "lsps2.buy";
|
||||
|
||||
pub(crate) const LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE: i32 = 200;
|
||||
|
||||
pub(crate) const LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE: i32 = 201;
|
||||
pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE: i32 = 202;
|
||||
pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE: i32 = 203;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
/// A request made to an LSP to learn their current channel fees and parameters.
|
||||
pub struct GetInfoRequest {
|
||||
/// An optional token to provide to the LSP.
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Fees and parameters for a JIT Channel without the promise.
|
||||
///
|
||||
/// The promise will be calculated automatically for the LSP and this type converted
|
||||
/// into an [`OpeningFeeParams`] for transit over the wire.
|
||||
pub struct RawOpeningFeeParams {
|
||||
/// The minimum fee required for the channel open.
|
||||
pub min_fee_msat: u64,
|
||||
/// A fee proportional to the size of the initial payment.
|
||||
pub proportional: u32,
|
||||
/// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid.
|
||||
pub valid_until: chrono::DateTime<Utc>,
|
||||
/// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing.
|
||||
pub min_lifetime: u32,
|
||||
/// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter.
|
||||
pub max_client_to_self_delay: u32,
|
||||
/// The minimum payment size that the LSP will accept when opening a channel.
|
||||
pub min_payment_size_msat: u64,
|
||||
/// The maximum payment size that the LSP will accept when opening a channel.
|
||||
pub max_payment_size_msat: u64,
|
||||
}
|
||||
|
||||
impl RawOpeningFeeParams {
|
||||
pub(crate) fn into_opening_fee_params(self, promise_secret: &[u8; 32]) -> OpeningFeeParams {
|
||||
let mut hmac = HmacEngine::<Sha256>::new(promise_secret);
|
||||
hmac.input(&self.min_fee_msat.to_be_bytes());
|
||||
hmac.input(&self.proportional.to_be_bytes());
|
||||
hmac.input(self.valid_until.to_rfc3339().as_bytes());
|
||||
hmac.input(&self.min_lifetime.to_be_bytes());
|
||||
hmac.input(&self.max_client_to_self_delay.to_be_bytes());
|
||||
hmac.input(&self.min_payment_size_msat.to_be_bytes());
|
||||
hmac.input(&self.max_payment_size_msat.to_be_bytes());
|
||||
let promise_bytes = Hmac::from_engine(hmac).to_byte_array();
|
||||
let promise = utils::hex_str(&promise_bytes[..]);
|
||||
OpeningFeeParams {
|
||||
min_fee_msat: self.min_fee_msat,
|
||||
proportional: self.proportional,
|
||||
valid_until: self.valid_until.clone(),
|
||||
min_lifetime: self.min_lifetime,
|
||||
max_client_to_self_delay: self.max_client_to_self_delay,
|
||||
min_payment_size_msat: self.min_payment_size_msat,
|
||||
max_payment_size_msat: self.max_payment_size_msat,
|
||||
promise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
/// Fees and parameters for a JIT Channel including the promise.
|
||||
///
|
||||
/// The promise is an HMAC calculated using a secret known to the LSP and the rest of the fields as input.
|
||||
/// It exists so the LSP can verify the authenticity of a client provided OpeningFeeParams by recalculating
|
||||
/// the promise using the secret. Once verified they can be confident it was not modified by the client.
|
||||
pub struct OpeningFeeParams {
|
||||
/// The minimum fee required for the channel open.
|
||||
#[serde(with = "string_amount")]
|
||||
pub min_fee_msat: u64,
|
||||
/// A fee proportional to the size of the initial payment.
|
||||
pub proportional: u32,
|
||||
/// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid.
|
||||
pub valid_until: chrono::DateTime<Utc>,
|
||||
/// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing.
|
||||
pub min_lifetime: u32,
|
||||
/// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter.
|
||||
pub max_client_to_self_delay: u32,
|
||||
/// The minimum payment size that the LSP will accept when opening a channel.
|
||||
#[serde(with = "string_amount")]
|
||||
pub min_payment_size_msat: u64,
|
||||
/// The maximum payment size that the LSP will accept when opening a channel.
|
||||
#[serde(with = "string_amount")]
|
||||
pub max_payment_size_msat: u64,
|
||||
/// The HMAC used to verify the authenticity of these parameters.
|
||||
pub promise: String,
|
||||
}
|
||||
|
||||
/// A response to a [`GetInfoRequest`]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct GetInfoResponse {
|
||||
/// A set of opening fee parameters.
|
||||
pub opening_fee_params_menu: Vec<OpeningFeeParams>,
|
||||
}
|
||||
|
||||
/// A request to buy a JIT channel.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BuyRequest {
|
||||
/// The fee parameters you would like to use.
|
||||
pub opening_fee_params: OpeningFeeParams,
|
||||
/// The size of the initial payment you expect to receive.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(with = "string_amount_option")]
|
||||
pub payment_size_msat: Option<u64>,
|
||||
}
|
||||
|
||||
/// A newtype that holds a `short_channel_id` in human readable format of BBBxTTTx000.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct InterceptScid(String);
|
||||
|
||||
impl From<u64> for InterceptScid {
|
||||
fn from(scid: u64) -> Self {
|
||||
let block = scid_utils::block_from_scid(scid);
|
||||
let tx_index = scid_utils::tx_index_from_scid(scid);
|
||||
let vout = scid_utils::vout_from_scid(scid);
|
||||
|
||||
Self(format!("{}x{}x{}", block, tx_index, vout))
|
||||
}
|
||||
}
|
||||
|
||||
impl InterceptScid {
|
||||
/// Try to convert a [`InterceptScid`] into a u64 used by LDK.
|
||||
pub fn to_scid(&self) -> Result<u64, ()> {
|
||||
utils::scid_from_human_readable_string(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A response to a [`BuyRequest`].
|
||||
///
|
||||
/// Includes information needed to construct an invoice.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BuyResponse {
|
||||
/// The intercept short channel id used by LSP to identify need to open channel.
|
||||
pub jit_channel_scid: InterceptScid,
|
||||
/// The locktime expiry delta the lsp requires.
|
||||
pub lsp_cltv_expiry_delta: u32,
|
||||
/// A flag that indicates who is trusting who.
|
||||
#[serde(default)]
|
||||
pub client_trusts_lsp: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// An enum that captures all the valid JSON-RPC requests in the LSPS2 protocol.
|
||||
pub enum LSPS2Request {
|
||||
/// A request to learn an LSP's channel fees and parameters.
|
||||
GetInfo(GetInfoRequest),
|
||||
/// A request to buy a JIT channel from an LSP.
|
||||
Buy(BuyRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// An enum that captures all the valid JSON-RPC responses in the LSPS2 protocol.
|
||||
pub enum LSPS2Response {
|
||||
/// A successful response to a [`LSPS2Request::GetInfo`] request.
|
||||
GetInfo(GetInfoResponse),
|
||||
/// An error response to a [`LSPS2Request::GetInfo`] request.
|
||||
GetInfoError(ResponseError),
|
||||
/// A successful response to a [`LSPS2Request::Buy`] request.
|
||||
Buy(BuyResponse),
|
||||
/// An error response to a [`LSPS2Request::Buy`] request.
|
||||
BuyError(ResponseError),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
/// An enum that captures all valid JSON-RPC messages in the LSPS2 protocol.
|
||||
pub enum LSPS2Message {
|
||||
/// An LSPS2 JSON-RPC request.
|
||||
Request(RequestId, LSPS2Request),
|
||||
/// An LSPS2 JSON-RPC response.
|
||||
Response(RequestId, LSPS2Response),
|
||||
}
|
||||
|
||||
impl TryFrom<LSPSMessage> for LSPS2Message {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(message: LSPSMessage) -> Result<Self, Self::Error> {
|
||||
if let LSPSMessage::LSPS2(message) = message {
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LSPS2Message> for LSPSMessage {
|
||||
fn from(message: LSPS2Message) -> Self {
|
||||
LSPSMessage::LSPS2(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::alloc::string::ToString;
|
||||
use crate::lsps2::utils::is_valid_opening_fee_params;
|
||||
|
||||
#[test]
|
||||
fn into_opening_fee_params_produces_valid_promise() {
|
||||
let min_fee_msat = 100;
|
||||
let proportional = 21;
|
||||
let valid_until: chrono::DateTime<Utc> =
|
||||
chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into();
|
||||
let min_lifetime = 144;
|
||||
let max_client_to_self_delay = 128;
|
||||
let min_payment_size_msat = 1;
|
||||
let max_payment_size_msat = 100_000_000;
|
||||
|
||||
let raw = RawOpeningFeeParams {
|
||||
min_fee_msat,
|
||||
proportional,
|
||||
valid_until: valid_until.clone().into(),
|
||||
min_lifetime,
|
||||
max_client_to_self_delay,
|
||||
min_payment_size_msat,
|
||||
max_payment_size_msat,
|
||||
};
|
||||
|
||||
let promise_secret = [1u8; 32];
|
||||
|
||||
let opening_fee_params = raw.into_opening_fee_params(&promise_secret);
|
||||
|
||||
assert_eq!(opening_fee_params.min_fee_msat, min_fee_msat);
|
||||
assert_eq!(opening_fee_params.proportional, proportional);
|
||||
assert_eq!(opening_fee_params.valid_until, valid_until);
|
||||
assert_eq!(opening_fee_params.min_lifetime, min_lifetime);
|
||||
assert_eq!(opening_fee_params.max_client_to_self_delay, max_client_to_self_delay);
|
||||
assert_eq!(opening_fee_params.min_payment_size_msat, min_payment_size_msat);
|
||||
assert_eq!(opening_fee_params.max_payment_size_msat, max_payment_size_msat);
|
||||
|
||||
assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_single_field_produced_invalid_params() {
|
||||
let min_fee_msat = 100;
|
||||
let proportional = 21;
|
||||
let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap();
|
||||
let min_lifetime = 144;
|
||||
let max_client_to_self_delay = 128;
|
||||
let min_payment_size_msat = 1;
|
||||
let max_payment_size_msat = 100_000_000;
|
||||
|
||||
let raw = RawOpeningFeeParams {
|
||||
min_fee_msat,
|
||||
proportional,
|
||||
valid_until: valid_until.into(),
|
||||
min_lifetime,
|
||||
max_client_to_self_delay,
|
||||
min_payment_size_msat,
|
||||
max_payment_size_msat,
|
||||
};
|
||||
|
||||
let promise_secret = [1u8; 32];
|
||||
|
||||
let mut opening_fee_params = raw.into_opening_fee_params(&promise_secret);
|
||||
opening_fee_params.min_fee_msat = min_fee_msat + 1;
|
||||
assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_secret_produced_invalid_params() {
|
||||
let min_fee_msat = 100;
|
||||
let proportional = 21;
|
||||
let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap();
|
||||
let min_lifetime = 144;
|
||||
let max_client_to_self_delay = 128;
|
||||
let min_payment_size_msat = 1;
|
||||
let max_payment_size_msat = 100_000_000;
|
||||
|
||||
let raw = RawOpeningFeeParams {
|
||||
min_fee_msat,
|
||||
proportional,
|
||||
valid_until: valid_until.into(),
|
||||
min_lifetime,
|
||||
max_client_to_self_delay,
|
||||
min_payment_size_msat,
|
||||
max_payment_size_msat,
|
||||
};
|
||||
|
||||
let promise_secret = [1u8; 32];
|
||||
let other_secret = [2u8; 32];
|
||||
|
||||
let opening_fee_params = raw.into_opening_fee_params(&promise_secret);
|
||||
assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
// TODO: We need to find a way to check expiry times in no-std builds.
|
||||
fn expired_params_produces_invalid_params() {
|
||||
let min_fee_msat = 100;
|
||||
let proportional = 21;
|
||||
let valid_until = chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap();
|
||||
let min_lifetime = 144;
|
||||
let max_client_to_self_delay = 128;
|
||||
let min_payment_size_msat = 1;
|
||||
let max_payment_size_msat = 100_000_000;
|
||||
|
||||
let raw = RawOpeningFeeParams {
|
||||
min_fee_msat,
|
||||
proportional,
|
||||
valid_until: valid_until.into(),
|
||||
min_lifetime,
|
||||
max_client_to_self_delay,
|
||||
min_payment_size_msat,
|
||||
max_payment_size_msat,
|
||||
};
|
||||
|
||||
let promise_secret = [1u8; 32];
|
||||
|
||||
let opening_fee_params = raw.into_opening_fee_params(&promise_secret);
|
||||
assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_request_serialization() {
|
||||
let min_fee_msat = 100;
|
||||
let proportional = 21;
|
||||
let valid_until = chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap();
|
||||
let min_lifetime = 144;
|
||||
let max_client_to_self_delay = 128;
|
||||
let min_payment_size_msat = 1;
|
||||
let max_payment_size_msat = 100_000_000;
|
||||
|
||||
let raw = RawOpeningFeeParams {
|
||||
min_fee_msat,
|
||||
proportional,
|
||||
valid_until: valid_until.into(),
|
||||
min_lifetime,
|
||||
max_client_to_self_delay,
|
||||
min_payment_size_msat,
|
||||
max_payment_size_msat,
|
||||
};
|
||||
|
||||
let promise_secret = [1u8; 32];
|
||||
|
||||
let opening_fee_params = raw.into_opening_fee_params(&promise_secret);
|
||||
let json_str = r#"{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"}"#;
|
||||
assert_eq!(json_str, serde_json::json!(opening_fee_params).to_string());
|
||||
assert_eq!(opening_fee_params, serde_json::from_str(json_str).unwrap());
|
||||
|
||||
let payment_size_msat = Some(1234);
|
||||
let buy_request_fixed =
|
||||
BuyRequest { opening_fee_params: opening_fee_params.clone(), payment_size_msat };
|
||||
let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"},"payment_size_msat":"1234"}"#;
|
||||
assert_eq!(json_str, serde_json::json!(buy_request_fixed).to_string());
|
||||
assert_eq!(buy_request_fixed, serde_json::from_str(json_str).unwrap());
|
||||
|
||||
let payment_size_msat = None;
|
||||
let buy_request_variable = BuyRequest { opening_fee_params, payment_size_msat };
|
||||
|
||||
// Check we skip serialization if payment_size_msat is None.
|
||||
let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"}}"#;
|
||||
assert_eq!(json_str, serde_json::json!(buy_request_variable).to_string());
|
||||
assert_eq!(buy_request_variable, serde_json::from_str(json_str).unwrap());
|
||||
|
||||
// Check we still deserialize correctly if payment_size_msat is 'null'.
|
||||
let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"},"payment_size_msat":null}"#;
|
||||
assert_eq!(buy_request_variable, serde_json::from_str(json_str).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_spec_test_vectors() {
|
||||
// Here, we simply assert that we're able to parse all examples given in LSPS2.
|
||||
let json_str = r#"{
|
||||
"opening_fee_params_menu": [
|
||||
{
|
||||
"min_fee_msat": "546000",
|
||||
"proportional": 1200,
|
||||
"valid_until": "2023-02-23T08:47:30.511Z",
|
||||
"min_lifetime": 1008,
|
||||
"max_client_to_self_delay": 2016,
|
||||
"min_payment_size_msat": "1000",
|
||||
"max_payment_size_msat": "1000000",
|
||||
"promise": "abcdefghijklmnopqrstuvwxyz"
|
||||
},
|
||||
{
|
||||
"min_fee_msat": "1092000",
|
||||
"proportional": 2400,
|
||||
"valid_until": "2023-02-27T21:23:57.984Z",
|
||||
"min_lifetime": 1008,
|
||||
"max_client_to_self_delay": 2016,
|
||||
"min_payment_size_msat": "1000",
|
||||
"max_payment_size_msat": "1000000",
|
||||
"promise": "abcdefghijklmnopqrstuvwxyz"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
let _get_info_response: GetInfoResponse = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"opening_fee_params": {
|
||||
"min_fee_msat": "546000",
|
||||
"proportional": 1200,
|
||||
"valid_until": "2023-02-23T08:47:30.511Z",
|
||||
"min_lifetime": 1008,
|
||||
"max_client_to_self_delay": 2016,
|
||||
"min_payment_size_msat": "1000",
|
||||
"max_payment_size_msat": "1000000",
|
||||
"promise": "abcdefghijklmnopqrstuvwxyz"
|
||||
},
|
||||
"payment_size_msat": "42000"
|
||||
}"#;
|
||||
let _buy_request: BuyRequest = serde_json::from_str(json_str).unwrap();
|
||||
|
||||
let json_str = r#"{
|
||||
"jit_channel_scid": "29451x4815x1",
|
||||
"lsp_cltv_expiry_delta" : 144,
|
||||
"client_trusts_lsp": false
|
||||
}"#;
|
||||
let _buy_response: BuyResponse = serde_json::from_str(json_str).unwrap();
|
||||
}
|
||||
}
|
119
lightning-liquidity/src/lsps2/payment_queue.rs
Normal file
119
lightning-liquidity/src/lsps2/payment_queue.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use crate::prelude::Vec;
|
||||
use lightning::ln::channelmanager::InterceptId;
|
||||
use lightning_types::payment::PaymentHash;
|
||||
|
||||
/// Holds payments with the corresponding HTLCs until it is possible to pay the fee.
|
||||
/// When the fee is successfully paid with a forwarded payment, the queue should be consumed and the
|
||||
/// remaining payments forwarded.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct PaymentQueue {
|
||||
payments: Vec<(PaymentHash, Vec<InterceptedHTLC>)>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct InterceptedHTLC {
|
||||
pub(crate) intercept_id: InterceptId,
|
||||
pub(crate) expected_outbound_amount_msat: u64,
|
||||
pub(crate) payment_hash: PaymentHash,
|
||||
}
|
||||
|
||||
impl PaymentQueue {
|
||||
pub(crate) fn new() -> PaymentQueue {
|
||||
PaymentQueue { payments: Vec::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn add_htlc(&mut self, new_htlc: InterceptedHTLC) -> (u64, usize) {
|
||||
let payment = self.payments.iter_mut().find(|(p, _)| p == &new_htlc.payment_hash);
|
||||
if let Some((payment_hash, htlcs)) = payment {
|
||||
// HTLCs within a payment should have the same payment hash.
|
||||
debug_assert!(htlcs.iter().all(|htlc| htlc.payment_hash == *payment_hash));
|
||||
// The given HTLC should not already be present.
|
||||
debug_assert!(htlcs.iter().all(|htlc| htlc.intercept_id != new_htlc.intercept_id));
|
||||
htlcs.push(new_htlc);
|
||||
let total_expected_outbound_amount_msat =
|
||||
htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum();
|
||||
(total_expected_outbound_amount_msat, htlcs.len())
|
||||
} else {
|
||||
let expected_outbound_amount_msat = new_htlc.expected_outbound_amount_msat;
|
||||
self.payments.push((new_htlc.payment_hash, vec![new_htlc]));
|
||||
(expected_outbound_amount_msat, 1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pop_greater_than_msat(
|
||||
&mut self, amount_msat: u64,
|
||||
) -> Option<(PaymentHash, Vec<InterceptedHTLC>)> {
|
||||
let position = self.payments.iter().position(|(_payment_hash, htlcs)| {
|
||||
htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum::<u64>() >= amount_msat
|
||||
});
|
||||
position.map(|position| self.payments.remove(position))
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) -> Vec<InterceptedHTLC> {
|
||||
self.payments.drain(..).map(|(_k, v)| v).flatten().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_payment_queue() {
|
||||
let mut payment_queue = PaymentQueue::new();
|
||||
assert_eq!(
|
||||
payment_queue.add_htlc(InterceptedHTLC {
|
||||
intercept_id: InterceptId([0; 32]),
|
||||
expected_outbound_amount_msat: 200_000_000,
|
||||
payment_hash: PaymentHash([100; 32]),
|
||||
}),
|
||||
(200_000_000, 1),
|
||||
);
|
||||
assert_eq!(payment_queue.pop_greater_than_msat(500_000_000), None);
|
||||
|
||||
assert_eq!(
|
||||
payment_queue.add_htlc(InterceptedHTLC {
|
||||
intercept_id: InterceptId([1; 32]),
|
||||
expected_outbound_amount_msat: 300_000_000,
|
||||
payment_hash: PaymentHash([101; 32]),
|
||||
}),
|
||||
(300_000_000, 1),
|
||||
);
|
||||
assert_eq!(payment_queue.pop_greater_than_msat(500_000_000), None);
|
||||
|
||||
assert_eq!(
|
||||
payment_queue.add_htlc(InterceptedHTLC {
|
||||
intercept_id: InterceptId([2; 32]),
|
||||
expected_outbound_amount_msat: 300_000_000,
|
||||
payment_hash: PaymentHash([100; 32]),
|
||||
}),
|
||||
(500_000_000, 2),
|
||||
);
|
||||
assert_eq!(
|
||||
payment_queue.pop_greater_than_msat(500_000_000),
|
||||
Some((
|
||||
PaymentHash([100; 32]),
|
||||
vec![
|
||||
InterceptedHTLC {
|
||||
intercept_id: InterceptId([0; 32]),
|
||||
expected_outbound_amount_msat: 200_000_000,
|
||||
payment_hash: PaymentHash([100; 32]),
|
||||
},
|
||||
InterceptedHTLC {
|
||||
intercept_id: InterceptId([2; 32]),
|
||||
expected_outbound_amount_msat: 300_000_000,
|
||||
payment_hash: PaymentHash([100; 32]),
|
||||
},
|
||||
]
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
payment_queue.clear(),
|
||||
vec![InterceptedHTLC {
|
||||
intercept_id: InterceptId([1; 32]),
|
||||
expected_outbound_amount_msat: 300_000_000,
|
||||
payment_hash: PaymentHash([101; 32]),
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
1845
lightning-liquidity/src/lsps2/service.rs
Normal file
1845
lightning-liquidity/src/lsps2/service.rs
Normal file
File diff suppressed because it is too large
Load diff
95
lightning-liquidity/src/lsps2/utils.rs
Normal file
95
lightning-liquidity/src/lsps2/utils.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
//! Utilities for implementing the LSPS2 standard.
|
||||
|
||||
use crate::lsps2::msgs::OpeningFeeParams;
|
||||
use crate::utils;
|
||||
|
||||
use bitcoin::hashes::hmac::{Hmac, HmacEngine};
|
||||
use bitcoin::hashes::sha256::Hash as Sha256;
|
||||
use bitcoin::hashes::{Hash, HashEngine};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Determines if the given parameters are valid given the secret used to generate the promise.
|
||||
pub fn is_valid_opening_fee_params(
|
||||
fee_params: &OpeningFeeParams, promise_secret: &[u8; 32],
|
||||
) -> bool {
|
||||
if is_expired_opening_fee_params(fee_params) {
|
||||
return false;
|
||||
}
|
||||
let mut hmac = HmacEngine::<Sha256>::new(promise_secret);
|
||||
hmac.input(&fee_params.min_fee_msat.to_be_bytes());
|
||||
hmac.input(&fee_params.proportional.to_be_bytes());
|
||||
hmac.input(fee_params.valid_until.to_rfc3339().as_bytes());
|
||||
hmac.input(&fee_params.min_lifetime.to_be_bytes());
|
||||
hmac.input(&fee_params.max_client_to_self_delay.to_be_bytes());
|
||||
hmac.input(&fee_params.min_payment_size_msat.to_be_bytes());
|
||||
hmac.input(&fee_params.max_payment_size_msat.to_be_bytes());
|
||||
let promise_bytes = Hmac::from_engine(hmac).to_byte_array();
|
||||
let promise = utils::hex_str(&promise_bytes[..]);
|
||||
promise == fee_params.promise
|
||||
}
|
||||
|
||||
/// Determines if the given parameters are expired, or still valid.
|
||||
#[cfg_attr(not(feature = "std"), allow(unused_variables))]
|
||||
pub fn is_expired_opening_fee_params(fee_params: &OpeningFeeParams) -> bool {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let seconds_since_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock to be ahead of the unix epoch")
|
||||
.as_secs();
|
||||
let valid_until_seconds_since_epoch = fee_params
|
||||
.valid_until
|
||||
.timestamp()
|
||||
.try_into()
|
||||
.expect("expiration to be ahead of unix epoch");
|
||||
seconds_since_epoch > valid_until_seconds_since_epoch
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
// TODO: We need to find a way to check expiry times in no-std builds.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the opening fee given a payment size and the fee parameters.
|
||||
///
|
||||
/// Returns [`Option::None`] when the computation overflows.
|
||||
///
|
||||
/// See the [`specification`](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#computing-the-opening_fee) for more details.
|
||||
pub fn compute_opening_fee(
|
||||
payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u64,
|
||||
) -> Option<u64> {
|
||||
payment_size_msat
|
||||
.checked_mul(opening_fee_proportional)
|
||||
.and_then(|f| f.checked_add(999999))
|
||||
.and_then(|f| f.checked_div(1000000))
|
||||
.map(|f| core::cmp::max(f, opening_fee_min_fee_msat))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000;
|
||||
|
||||
fn arb_opening_fee_params() -> impl Strategy<Value = (u64, u64, u64)> {
|
||||
(0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_compute_opening_fee((payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) in arb_opening_fee_params()) {
|
||||
if let Some(res) = compute_opening_fee(payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) {
|
||||
assert!(res >= opening_fee_min_fee_msat);
|
||||
assert_eq!(res as f32, (payment_size_msat as f32 * opening_fee_proportional as f32));
|
||||
} else {
|
||||
// Check we actually overflowed.
|
||||
let max_value = u64::MAX as u128;
|
||||
assert!((payment_size_msat as u128 * opening_fee_proportional as u128) > max_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
696
lightning-liquidity/src/manager.rs
Normal file
696
lightning-liquidity/src/manager.rs
Normal file
|
@ -0,0 +1,696 @@
|
|||
use crate::events::{Event, EventQueue};
|
||||
use crate::lsps0::client::LSPS0ClientHandler;
|
||||
use crate::lsps0::msgs::LSPS0Message;
|
||||
use crate::lsps0::ser::{
|
||||
LSPSMessage, LSPSMethod, ProtocolMessageHandler, RawLSPSMessage, RequestId, ResponseError,
|
||||
JSONRPC_INVALID_MESSAGE_ERROR_CODE, JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE,
|
||||
LSPS_MESSAGE_TYPE_ID,
|
||||
};
|
||||
use crate::lsps0::service::LSPS0ServiceHandler;
|
||||
use crate::message_queue::MessageQueue;
|
||||
|
||||
use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler};
|
||||
use crate::lsps1::msgs::LSPS1Message;
|
||||
#[cfg(lsps1_service)]
|
||||
use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler};
|
||||
|
||||
use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler};
|
||||
use crate::lsps2::msgs::LSPS2Message;
|
||||
use crate::lsps2::service::{LSPS2ServiceConfig, LSPS2ServiceHandler};
|
||||
use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet, ToString, Vec};
|
||||
use crate::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use lightning::chain::{self, BestBlock, Confirm, Filter, Listen};
|
||||
use lightning::ln::channelmanager::{AChannelManager, ChainParameters};
|
||||
use lightning::ln::msgs::{ErrorAction, LightningError};
|
||||
use lightning::ln::peer_handler::CustomMessageHandler;
|
||||
use lightning::ln::wire::CustomMessageReader;
|
||||
use lightning::sign::EntropySource;
|
||||
use lightning::util::logger::Level;
|
||||
use lightning::util::ser::Readable;
|
||||
|
||||
use lightning_types::features::{InitFeatures, NodeFeatures};
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
use core::ops::Deref;
|
||||
|
||||
const LSPS_FEATURE_BIT: usize = 729;
|
||||
|
||||
/// A server-side configuration for [`LiquidityManager`].
|
||||
///
|
||||
/// Allows end-users to configure options when using the [`LiquidityManager`]
|
||||
/// to provide liquidity services to clients.
|
||||
pub struct LiquidityServiceConfig {
|
||||
/// Optional server-side configuration for LSPS1 channel requests.
|
||||
#[cfg(lsps1_service)]
|
||||
pub lsps1_service_config: Option<LSPS1ServiceConfig>,
|
||||
/// Optional server-side configuration for JIT channels
|
||||
/// should you want to support them.
|
||||
pub lsps2_service_config: Option<LSPS2ServiceConfig>,
|
||||
/// Controls whether the liquidity service should be advertised via setting the feature bit in
|
||||
/// node announcment and the init message.
|
||||
pub advertise_service: bool,
|
||||
}
|
||||
|
||||
/// A client-side configuration for [`LiquidityManager`].
|
||||
///
|
||||
/// Allows end-user to configure options when using the [`LiquidityManager`]
|
||||
/// to access liquidity services from a provider.
|
||||
pub struct LiquidityClientConfig {
|
||||
/// Optional client-side configuration for LSPS1 channel requests.
|
||||
pub lsps1_client_config: Option<LSPS1ClientConfig>,
|
||||
/// Optional client-side configuration for JIT channels.
|
||||
pub lsps2_client_config: Option<LSPS2ClientConfig>,
|
||||
}
|
||||
|
||||
/// The main interface into LSP functionality.
|
||||
///
|
||||
/// Should be used as a [`CustomMessageHandler`] for your [`PeerManager`]'s [`MessageHandler`].
|
||||
///
|
||||
/// Users should provide a callback to process queued messages via
|
||||
/// [`LiquidityManager::set_process_msgs_callback`] post construction. This allows the
|
||||
/// [`LiquidityManager`] to wake the [`PeerManager`] when there are pending messages to be sent.
|
||||
///
|
||||
/// Users need to continually poll [`LiquidityManager::get_and_clear_pending_events`] in order to surface
|
||||
/// [`Event`]'s that likely need to be handled.
|
||||
///
|
||||
/// If configured, users must forward the [`Event::HTLCIntercepted`] event parameters to [`LSPS2ServiceHandler::htlc_intercepted`]
|
||||
/// and the [`Event::ChannelReady`] event parameters to [`LSPS2ServiceHandler::channel_ready`].
|
||||
///
|
||||
/// [`PeerManager`]: lightning::ln::peer_handler::PeerManager
|
||||
/// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler
|
||||
/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted
|
||||
/// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady
|
||||
pub struct LiquidityManager<ES: Deref + Clone, CM: Deref + Clone, C: Deref + Clone>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
pending_messages: Arc<MessageQueue>,
|
||||
pending_events: Arc<EventQueue>,
|
||||
request_id_to_method_map: Mutex<HashMap<RequestId, LSPSMethod>>,
|
||||
// We ignore peers if they send us bogus data.
|
||||
ignored_peers: RwLock<HashSet<PublicKey>>,
|
||||
lsps0_client_handler: LSPS0ClientHandler<ES>,
|
||||
lsps0_service_handler: Option<LSPS0ServiceHandler>,
|
||||
#[cfg(lsps1_service)]
|
||||
lsps1_service_handler: Option<LSPS1ServiceHandler<ES, CM, C>>,
|
||||
lsps1_client_handler: Option<LSPS1ClientHandler<ES>>,
|
||||
lsps2_service_handler: Option<LSPS2ServiceHandler<CM>>,
|
||||
lsps2_client_handler: Option<LSPS2ClientHandler<ES>>,
|
||||
service_config: Option<LiquidityServiceConfig>,
|
||||
_client_config: Option<LiquidityClientConfig>,
|
||||
best_block: Option<RwLock<BestBlock>>,
|
||||
_chain_source: Option<C>,
|
||||
}
|
||||
|
||||
impl<ES: Deref + Clone, CM: Deref + Clone, C: Deref + Clone> LiquidityManager<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
/// Constructor for the [`LiquidityManager`].
|
||||
///
|
||||
/// Sets up the required protocol message handlers based on the given
|
||||
/// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`].
|
||||
pub fn new(
|
||||
entropy_source: ES, channel_manager: CM, chain_source: Option<C>,
|
||||
chain_params: Option<ChainParameters>, service_config: Option<LiquidityServiceConfig>,
|
||||
client_config: Option<LiquidityClientConfig>,
|
||||
) -> Self
|
||||
where {
|
||||
let pending_messages = Arc::new(MessageQueue::new());
|
||||
let pending_events = Arc::new(EventQueue::new());
|
||||
let ignored_peers = RwLock::new(new_hash_set());
|
||||
|
||||
let mut supported_protocols = Vec::new();
|
||||
|
||||
let lsps2_client_handler = client_config.as_ref().and_then(|config| {
|
||||
config.lsps2_client_config.map(|config| {
|
||||
LSPS2ClientHandler::new(
|
||||
entropy_source.clone(),
|
||||
Arc::clone(&pending_messages),
|
||||
Arc::clone(&pending_events),
|
||||
config.clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
let lsps2_service_handler = service_config.as_ref().and_then(|config| {
|
||||
config.lsps2_service_config.as_ref().map(|config| {
|
||||
if let Some(number) =
|
||||
<LSPS2ServiceHandler<CM> as ProtocolMessageHandler>::PROTOCOL_NUMBER
|
||||
{
|
||||
supported_protocols.push(number);
|
||||
}
|
||||
LSPS2ServiceHandler::new(
|
||||
Arc::clone(&pending_messages),
|
||||
Arc::clone(&pending_events),
|
||||
channel_manager.clone(),
|
||||
config.clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let lsps1_client_handler = client_config.as_ref().and_then(|config| {
|
||||
config.lsps1_client_config.as_ref().map(|config| {
|
||||
LSPS1ClientHandler::new(
|
||||
entropy_source.clone(),
|
||||
Arc::clone(&pending_messages),
|
||||
Arc::clone(&pending_events),
|
||||
config.clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(lsps1_service)]
|
||||
let lsps1_service_handler = service_config.as_ref().and_then(|config| {
|
||||
if let Some(number) =
|
||||
<LSPS1ServiceHandler<ES> as ProtocolMessageHandler>::PROTOCOL_NUMBER
|
||||
{
|
||||
supported_protocols.push(number);
|
||||
}
|
||||
config.lsps1_service_config.as_ref().map(|config| {
|
||||
LSPS1ServiceHandler::new(
|
||||
entropy_source.clone(),
|
||||
Arc::clone(&pending_messages),
|
||||
Arc::clone(&pending_events),
|
||||
channel_manager.clone(),
|
||||
chain_source.clone(),
|
||||
config.clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let lsps0_client_handler = LSPS0ClientHandler::new(
|
||||
entropy_source.clone(),
|
||||
Arc::clone(&pending_messages),
|
||||
Arc::clone(&pending_events),
|
||||
);
|
||||
|
||||
let lsps0_service_handler = if service_config.is_some() {
|
||||
Some(LSPS0ServiceHandler::new(vec![], Arc::clone(&pending_messages)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
pending_messages,
|
||||
pending_events,
|
||||
request_id_to_method_map: Mutex::new(new_hash_map()),
|
||||
ignored_peers,
|
||||
lsps0_client_handler,
|
||||
lsps0_service_handler,
|
||||
lsps1_client_handler,
|
||||
#[cfg(lsps1_service)]
|
||||
lsps1_service_handler,
|
||||
lsps2_client_handler,
|
||||
lsps2_service_handler,
|
||||
service_config,
|
||||
_client_config: client_config,
|
||||
best_block: chain_params.map(|chain_params| RwLock::new(chain_params.best_block)),
|
||||
_chain_source: chain_source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS0 client-side handler.
|
||||
pub fn lsps0_client_handler(&self) -> &LSPS0ClientHandler<ES> {
|
||||
&self.lsps0_client_handler
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS0 server-side handler.
|
||||
pub fn lsps0_service_handler(&self) -> Option<&LSPS0ServiceHandler> {
|
||||
self.lsps0_service_handler.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS1 client-side handler.
|
||||
///
|
||||
/// The returned hendler allows to initiate the LSPS1 client-side flow, i.e., allows to request
|
||||
/// channels from the configured LSP.
|
||||
pub fn lsps1_client_handler(&self) -> Option<&LSPS1ClientHandler<ES>> {
|
||||
self.lsps1_client_handler.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS1 server-side handler.
|
||||
#[cfg(lsps1_service)]
|
||||
pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler<ES, CM, C>> {
|
||||
self.lsps1_service_handler.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS2 client-side handler.
|
||||
///
|
||||
/// The returned hendler allows to initiate the LSPS2 client-side flow. That is, it allows to
|
||||
/// retrieve all necessary data to create 'just-in-time' invoices that, when paid, will have
|
||||
/// the configured LSP open a 'just-in-time' channel.
|
||||
pub fn lsps2_client_handler(&self) -> Option<&LSPS2ClientHandler<ES>> {
|
||||
self.lsps2_client_handler.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a reference to the LSPS2 server-side handler.
|
||||
///
|
||||
/// The returned hendler allows to initiate the LSPS2 service-side flow.
|
||||
pub fn lsps2_service_handler(&self) -> Option<&LSPS2ServiceHandler<CM>> {
|
||||
self.lsps2_service_handler.as_ref()
|
||||
}
|
||||
|
||||
/// Allows to set a callback that will be called after new messages are pushed to the message
|
||||
/// queue.
|
||||
///
|
||||
/// Usually, you'll want to use this to call [`PeerManager::process_events`] to clear the
|
||||
/// message queue. For example:
|
||||
///
|
||||
/// ```
|
||||
/// # use lightning::io;
|
||||
/// # use lightning_liquidity::LiquidityManager;
|
||||
/// # use std::sync::{Arc, RwLock};
|
||||
/// # use std::sync::atomic::{AtomicBool, Ordering};
|
||||
/// # use std::time::SystemTime;
|
||||
/// # struct MyStore {}
|
||||
/// # impl lightning::util::persist::KVStore for MyStore {
|
||||
/// # fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> io::Result<Vec<u8>> { Ok(Vec::new()) }
|
||||
/// # fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8]) -> io::Result<()> { Ok(()) }
|
||||
/// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) }
|
||||
/// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result<Vec<String>> { Ok(Vec::new()) }
|
||||
/// # }
|
||||
/// # struct MyEntropySource {}
|
||||
/// # impl lightning::sign::EntropySource for MyEntropySource {
|
||||
/// # fn get_secure_random_bytes(&self) -> [u8; 32] { [0u8; 32] }
|
||||
/// # }
|
||||
/// # struct MyEventHandler {}
|
||||
/// # impl MyEventHandler {
|
||||
/// # async fn handle_event(&self, _: lightning::events::Event) {}
|
||||
/// # }
|
||||
/// # #[derive(Eq, PartialEq, Clone, Hash)]
|
||||
/// # struct MySocketDescriptor {}
|
||||
/// # impl lightning::ln::peer_handler::SocketDescriptor for MySocketDescriptor {
|
||||
/// # fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { 0 }
|
||||
/// # fn disconnect_socket(&mut self) {}
|
||||
/// # }
|
||||
/// # type MyBroadcaster = dyn lightning::chain::chaininterface::BroadcasterInterface + Send + Sync;
|
||||
/// # type MyFeeEstimator = dyn lightning::chain::chaininterface::FeeEstimator + Send + Sync;
|
||||
/// # type MyNodeSigner = dyn lightning::sign::NodeSigner + Send + Sync;
|
||||
/// # type MyUtxoLookup = dyn lightning::routing::utxo::UtxoLookup + Send + Sync;
|
||||
/// # type MyFilter = dyn lightning::chain::Filter + Send + Sync;
|
||||
/// # type MyLogger = dyn lightning::util::logger::Logger + Send + Sync;
|
||||
/// # type MyChainMonitor = lightning::chain::chainmonitor::ChainMonitor<lightning::sign::InMemorySigner, Arc<MyFilter>, Arc<MyBroadcaster>, Arc<MyFeeEstimator>, Arc<MyLogger>, Arc<MyStore>>;
|
||||
/// # type MyPeerManager = lightning::ln::peer_handler::SimpleArcPeerManager<MySocketDescriptor, MyChainMonitor, MyBroadcaster, MyFeeEstimator, Arc<MyUtxoLookup>, MyLogger>;
|
||||
/// # type MyNetworkGraph = lightning::routing::gossip::NetworkGraph<Arc<MyLogger>>;
|
||||
/// # type MyGossipSync = lightning::routing::gossip::P2PGossipSync<Arc<MyNetworkGraph>, Arc<MyUtxoLookup>, Arc<MyLogger>>;
|
||||
/// # type MyChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager<MyChainMonitor, MyBroadcaster, MyFeeEstimator, MyLogger>;
|
||||
/// # type MyScorer = RwLock<lightning::routing::scoring::ProbabilisticScorer<Arc<MyNetworkGraph>, Arc<MyLogger>>>;
|
||||
/// # type MyLiquidityManager = LiquidityManager<Arc<MyEntropySource>, Arc<MyChannelManager>, Arc<MyFilter>>;
|
||||
/// # fn setup_background_processing(my_persister: Arc<MyStore>, my_event_handler: Arc<MyEventHandler>, my_chain_monitor: Arc<MyChainMonitor>, my_channel_manager: Arc<MyChannelManager>, my_logger: Arc<MyLogger>, my_peer_manager: Arc<MyPeerManager>, my_liquidity_manager: Arc<MyLiquidityManager>) {
|
||||
/// let process_msgs_pm = Arc::clone(&my_peer_manager);
|
||||
/// let process_msgs_callback = move || process_msgs_pm.process_events();
|
||||
///
|
||||
/// my_liquidity_manager.set_process_msgs_callback(process_msgs_callback);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events
|
||||
#[cfg(feature = "std")]
|
||||
pub fn set_process_msgs_callback(&self, callback: impl Fn() + Send + Sync + 'static) {
|
||||
self.pending_messages.set_process_msgs_callback(callback)
|
||||
}
|
||||
|
||||
/// Allows to set a callback that will be called after new messages are pushed to the message
|
||||
/// queue.
|
||||
///
|
||||
/// Usually, you'll want to use this to call [`PeerManager::process_events`] to clear the
|
||||
/// message queue. For example:
|
||||
///
|
||||
/// ```
|
||||
/// # use lightning::io;
|
||||
/// # use lightning_liquidity::LiquidityManager;
|
||||
/// # use std::sync::{Arc, RwLock};
|
||||
/// # use std::sync::atomic::{AtomicBool, Ordering};
|
||||
/// # use std::time::SystemTime;
|
||||
/// # struct MyStore {}
|
||||
/// # impl lightning::util::persist::KVStore for MyStore {
|
||||
/// # fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> io::Result<Vec<u8>> { Ok(Vec::new()) }
|
||||
/// # fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8]) -> io::Result<()> { Ok(()) }
|
||||
/// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) }
|
||||
/// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result<Vec<String>> { Ok(Vec::new()) }
|
||||
/// # }
|
||||
/// # struct MyEntropySource {}
|
||||
/// # impl lightning::sign::EntropySource for MyEntropySource {
|
||||
/// # fn get_secure_random_bytes(&self) -> [u8; 32] { [0u8; 32] }
|
||||
/// # }
|
||||
/// # struct MyEventHandler {}
|
||||
/// # impl MyEventHandler {
|
||||
/// # async fn handle_event(&self, _: lightning::events::Event) {}
|
||||
/// # }
|
||||
/// # #[derive(Eq, PartialEq, Clone, Hash)]
|
||||
/// # struct MySocketDescriptor {}
|
||||
/// # impl lightning::ln::peer_handler::SocketDescriptor for MySocketDescriptor {
|
||||
/// # fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { 0 }
|
||||
/// # fn disconnect_socket(&mut self) {}
|
||||
/// # }
|
||||
/// # type MyBroadcaster = dyn lightning::chain::chaininterface::BroadcasterInterface;
|
||||
/// # type MyFeeEstimator = dyn lightning::chain::chaininterface::FeeEstimator;
|
||||
/// # type MyNodeSigner = dyn lightning::sign::NodeSigner;
|
||||
/// # type MyUtxoLookup = dyn lightning::routing::utxo::UtxoLookup;
|
||||
/// # type MyFilter = dyn lightning::chain::Filter;
|
||||
/// # type MyLogger = dyn lightning::util::logger::Logger;
|
||||
/// # type MyChainMonitor = lightning::chain::chainmonitor::ChainMonitor<lightning::sign::InMemorySigner, Arc<MyFilter>, Arc<MyBroadcaster>, Arc<MyFeeEstimator>, Arc<MyLogger>, Arc<MyStore>>;
|
||||
/// # type MyPeerManager = lightning::ln::peer_handler::SimpleArcPeerManager<MySocketDescriptor, MyChainMonitor, MyBroadcaster, MyFeeEstimator, Arc<MyUtxoLookup>, MyLogger>;
|
||||
/// # type MyNetworkGraph = lightning::routing::gossip::NetworkGraph<Arc<MyLogger>>;
|
||||
/// # type MyGossipSync = lightning::routing::gossip::P2PGossipSync<Arc<MyNetworkGraph>, Arc<MyUtxoLookup>, Arc<MyLogger>>;
|
||||
/// # type MyChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager<MyChainMonitor, MyBroadcaster, MyFeeEstimator, MyLogger>;
|
||||
/// # type MyScorer = RwLock<lightning::routing::scoring::ProbabilisticScorer<Arc<MyNetworkGraph>, Arc<MyLogger>>>;
|
||||
/// # type MyLiquidityManager = LiquidityManager<Arc<MyEntropySource>, Arc<MyChannelManager>, Arc<MyFilter>>;
|
||||
/// # fn setup_background_processing(my_persister: Arc<MyStore>, my_event_handler: Arc<MyEventHandler>, my_chain_monitor: Arc<MyChainMonitor>, my_channel_manager: Arc<MyChannelManager>, my_logger: Arc<MyLogger>, my_peer_manager: Arc<MyPeerManager>, my_liquidity_manager: Arc<MyLiquidityManager>) {
|
||||
/// let process_msgs_pm = Arc::clone(&my_peer_manager);
|
||||
/// let process_msgs_callback = move || process_msgs_pm.process_events();
|
||||
///
|
||||
/// my_liquidity_manager.set_process_msgs_callback(process_msgs_callback);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub fn set_process_msgs_callback(&self, callback: impl Fn() + 'static) {
|
||||
self.pending_messages.set_process_msgs_callback(callback)
|
||||
}
|
||||
|
||||
/// Blocks the current thread until next event is ready and returns it.
|
||||
///
|
||||
/// Typically you would spawn a thread or task that calls this in a loop.
|
||||
///
|
||||
/// **Note**: Users must handle events as soon as possible to avoid an increased event queue
|
||||
/// memory footprint. We will start dropping any generated events after
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`] has been reached.
|
||||
///
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE
|
||||
#[cfg(feature = "std")]
|
||||
pub fn wait_next_event(&self) -> Event {
|
||||
self.pending_events.wait_next_event()
|
||||
}
|
||||
|
||||
/// Returns `Some` if an event is ready.
|
||||
///
|
||||
/// Typically you would spawn a thread or task that calls this in a loop.
|
||||
///
|
||||
/// **Note**: Users must handle events as soon as possible to avoid an increased event queue
|
||||
/// memory footprint. We will start dropping any generated events after
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`] has been reached.
|
||||
///
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE
|
||||
pub fn next_event(&self) -> Option<Event> {
|
||||
self.pending_events.next_event()
|
||||
}
|
||||
|
||||
/// Asynchronously polls the event queue and returns once the next event is ready.
|
||||
///
|
||||
/// Typically you would spawn a thread or task that calls this in a loop.
|
||||
///
|
||||
/// **Note**: Users must handle events as soon as possible to avoid an increased event queue
|
||||
/// memory footprint. We will start dropping any generated events after
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`] has been reached.
|
||||
///
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE
|
||||
pub async fn next_event_async(&self) -> Event {
|
||||
self.pending_events.next_event_async().await
|
||||
}
|
||||
|
||||
/// Returns and clears all events without blocking.
|
||||
///
|
||||
/// Typically you would spawn a thread or task that calls this in a loop.
|
||||
///
|
||||
/// **Note**: Users must handle events as soon as possible to avoid an increased event queue
|
||||
/// memory footprint. We will start dropping any generated events after
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`] has been reached.
|
||||
///
|
||||
/// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE
|
||||
pub fn get_and_clear_pending_events(&self) -> Vec<Event> {
|
||||
self.pending_events.get_and_clear_pending_events()
|
||||
}
|
||||
|
||||
fn handle_lsps_message(
|
||||
&self, msg: LSPSMessage, sender_node_id: &PublicKey,
|
||||
) -> Result<(), lightning::ln::msgs::LightningError> {
|
||||
match msg {
|
||||
LSPSMessage::Invalid(_error) => {
|
||||
return Err(LightningError { err: format!("{} did not understand a message we previously sent, maybe they don't support a protocol we are trying to use?", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Error)});
|
||||
},
|
||||
LSPSMessage::LSPS0(msg @ LSPS0Message::Response(..)) => {
|
||||
self.lsps0_client_handler.handle_message(msg, sender_node_id)?;
|
||||
},
|
||||
LSPSMessage::LSPS0(msg @ LSPS0Message::Request(..)) => {
|
||||
match &self.lsps0_service_handler {
|
||||
Some(lsps0_service_handler) => {
|
||||
lsps0_service_handler.handle_message(msg, sender_node_id)?;
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received LSPS0 request message without LSPS0 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS1(msg @ LSPS1Message::Response(..)) => {
|
||||
match &self.lsps1_client_handler {
|
||||
Some(lsps1_client_handler) => {
|
||||
lsps1_client_handler.handle_message(msg, sender_node_id)?;
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received LSPS1 response message without LSPS1 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS1(_msg @ LSPS1Message::Request(..)) => {
|
||||
#[cfg(lsps1_service)]
|
||||
match &self.lsps1_service_handler {
|
||||
Some(lsps1_service_handler) => {
|
||||
lsps1_service_handler.handle_message(_msg, sender_node_id)?;
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
#[cfg(not(lsps1_service))]
|
||||
return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
LSPSMessage::LSPS2(msg @ LSPS2Message::Response(..)) => {
|
||||
match &self.lsps2_client_handler {
|
||||
Some(lsps2_client_handler) => {
|
||||
lsps2_client_handler.handle_message(msg, sender_node_id)?;
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received LSPS2 response message without LSPS2 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
},
|
||||
LSPSMessage::LSPS2(msg @ LSPS2Message::Request(..)) => {
|
||||
match &self.lsps2_service_handler {
|
||||
Some(lsps2_service_handler) => {
|
||||
lsps2_service_handler.handle_message(msg, sender_node_id)?;
|
||||
},
|
||||
None => {
|
||||
return Err(LightningError { err: format!("Received LSPS2 request message without LSPS2 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)});
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref + Clone + Clone, CM: Deref + Clone, C: Deref + Clone> CustomMessageReader
|
||||
for LiquidityManager<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
type CustomMessage = RawLSPSMessage;
|
||||
|
||||
fn read<RD: lightning::io::Read>(
|
||||
&self, message_type: u16, buffer: &mut RD,
|
||||
) -> Result<Option<Self::CustomMessage>, lightning::ln::msgs::DecodeError> {
|
||||
match message_type {
|
||||
LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref + Clone, CM: Deref + Clone, C: Deref + Clone> CustomMessageHandler
|
||||
for LiquidityManager<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
fn handle_custom_message(
|
||||
&self, msg: Self::CustomMessage, sender_node_id: PublicKey,
|
||||
) -> Result<(), lightning::ln::msgs::LightningError> {
|
||||
{
|
||||
if self.ignored_peers.read().unwrap().contains(&sender_node_id) {
|
||||
let err = format!("Ignoring message from peer {}.", sender_node_id);
|
||||
return Err(LightningError {
|
||||
err,
|
||||
action: ErrorAction::IgnoreAndLog(Level::Trace),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let message = {
|
||||
{
|
||||
let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap();
|
||||
LSPSMessage::from_str_with_id_map(&msg.payload, &mut request_id_to_method_map)
|
||||
}
|
||||
.map_err(|_| {
|
||||
let error = ResponseError {
|
||||
code: JSONRPC_INVALID_MESSAGE_ERROR_CODE,
|
||||
message: JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE.to_string(),
|
||||
data: None,
|
||||
};
|
||||
|
||||
self.pending_messages.enqueue(&sender_node_id, LSPSMessage::Invalid(error));
|
||||
self.ignored_peers.write().unwrap().insert(sender_node_id);
|
||||
let err = format!(
|
||||
"Failed to deserialize invalid LSPS message. Ignoring peer {} from now on.",
|
||||
sender_node_id
|
||||
);
|
||||
LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Info) }
|
||||
})?
|
||||
};
|
||||
|
||||
self.handle_lsps_message(message, &sender_node_id)
|
||||
}
|
||||
|
||||
fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Self::CustomMessage)> {
|
||||
let pending_messages = self.pending_messages.get_and_clear_pending_msgs();
|
||||
|
||||
let mut request_ids_and_methods = pending_messages
|
||||
.iter()
|
||||
.filter_map(|(_, msg)| msg.get_request_id_and_method())
|
||||
.peekable();
|
||||
|
||||
if request_ids_and_methods.peek().is_some() {
|
||||
let mut request_id_to_method_map_lock = self.request_id_to_method_map.lock().unwrap();
|
||||
for (request_id, method) in request_ids_and_methods {
|
||||
request_id_to_method_map_lock.insert(request_id, method);
|
||||
}
|
||||
}
|
||||
|
||||
pending_messages
|
||||
.into_iter()
|
||||
.filter_map(|(public_key, msg)| {
|
||||
serde_json::to_string(&msg)
|
||||
.ok()
|
||||
.map(|payload| (public_key, RawLSPSMessage { payload }))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn provided_node_features(&self) -> NodeFeatures {
|
||||
let mut features = NodeFeatures::empty();
|
||||
|
||||
let advertise_service = self.service_config.as_ref().map_or(false, |c| c.advertise_service);
|
||||
|
||||
if advertise_service {
|
||||
features
|
||||
.set_optional_custom_bit(LSPS_FEATURE_BIT)
|
||||
.expect("Failed to set LSPS feature bit");
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
fn provided_init_features(&self, _their_node_id: PublicKey) -> InitFeatures {
|
||||
let mut features = InitFeatures::empty();
|
||||
|
||||
let advertise_service = self.service_config.as_ref().map_or(false, |c| c.advertise_service);
|
||||
if advertise_service {
|
||||
features
|
||||
.set_optional_custom_bit(LSPS_FEATURE_BIT)
|
||||
.expect("Failed to set LSPS feature bit");
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
fn peer_disconnected(&self, counterparty_node_id: bitcoin::secp256k1::PublicKey) {
|
||||
if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() {
|
||||
lsps2_service_handler.peer_disconnected(counterparty_node_id);
|
||||
}
|
||||
}
|
||||
fn peer_connected(
|
||||
&self, _: bitcoin::secp256k1::PublicKey, _: &lightning::ln::msgs::Init, _: bool,
|
||||
) -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref + Clone, CM: Deref + Clone, C: Deref + Clone> Listen for LiquidityManager<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
fn filtered_block_connected(
|
||||
&self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData,
|
||||
height: u32,
|
||||
) {
|
||||
if let Some(best_block) = &self.best_block {
|
||||
let best_block = best_block.read().unwrap();
|
||||
assert_eq!(best_block.block_hash, header.prev_blockhash,
|
||||
"Blocks must be connected in chain-order - the connected header must build on the last connected header");
|
||||
assert_eq!(best_block.height, height - 1,
|
||||
"Blocks must be connected in chain-order - the connected block height must be one greater than the previous height");
|
||||
}
|
||||
|
||||
self.transactions_confirmed(header, txdata, height);
|
||||
self.best_block_updated(header, height);
|
||||
}
|
||||
|
||||
fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) {
|
||||
let new_height = height - 1;
|
||||
if let Some(best_block) = &self.best_block {
|
||||
let mut best_block = best_block.write().unwrap();
|
||||
assert_eq!(best_block.block_hash, header.block_hash(),
|
||||
"Blocks must be disconnected in chain-order - the disconnected header must be the last connected header");
|
||||
assert_eq!(best_block.height, height,
|
||||
"Blocks must be disconnected in chain-order - the disconnected block must have the correct height");
|
||||
*best_block = BestBlock::new(header.prev_blockhash, new_height)
|
||||
}
|
||||
|
||||
// TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler.
|
||||
// Internally this should call transaction_unconfirmed for all transactions that were
|
||||
// confirmed at a height <= the one we now disconnected.
|
||||
}
|
||||
}
|
||||
|
||||
impl<ES: Deref + Clone, CM: Deref + Clone, C: Deref + Clone> Confirm for LiquidityManager<ES, CM, C>
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
CM::Target: AChannelManager,
|
||||
C::Target: Filter,
|
||||
{
|
||||
fn transactions_confirmed(
|
||||
&self, _header: &bitcoin::block::Header, _txdata: &chain::transaction::TransactionData,
|
||||
_height: u32,
|
||||
) {
|
||||
// TODO: Call transactions_confirmed on all sub-modules that require it, e.g., LSPS1MessageHandler.
|
||||
}
|
||||
|
||||
fn transaction_unconfirmed(&self, _txid: &bitcoin::Txid) {
|
||||
// TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., LSPS1MessageHandler.
|
||||
// Internally this should call transaction_unconfirmed for all transactions that were
|
||||
// confirmed at a height <= the one we now unconfirmed.
|
||||
}
|
||||
|
||||
fn best_block_updated(&self, _header: &bitcoin::block::Header, _height: u32) {
|
||||
// TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler.
|
||||
if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() {
|
||||
lsps2_service_handler.prune_peer_state();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option<bitcoin::BlockHash>)> {
|
||||
// TODO: Collect relevant txids from all sub-modules that, e.g., LSPS1MessageHandler.
|
||||
Vec::new()
|
||||
}
|
||||
}
|
51
lightning-liquidity/src/message_queue.rs
Normal file
51
lightning-liquidity/src/message_queue.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
//! Holds types and traits used to implement message queues for [`LSPSMessage`]s.
|
||||
|
||||
use crate::lsps0::ser::LSPSMessage;
|
||||
use crate::prelude::{Box, Vec, VecDeque};
|
||||
use crate::sync::{Mutex, RwLock};
|
||||
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
|
||||
/// The default [`MessageQueue`] Implementation used by [`LiquidityManager`].
|
||||
///
|
||||
/// [`LiquidityManager`]: crate::LiquidityManager
|
||||
pub struct MessageQueue {
|
||||
queue: Mutex<VecDeque<(PublicKey, LSPSMessage)>>,
|
||||
#[cfg(feature = "std")]
|
||||
process_msgs_callback: RwLock<Option<Box<dyn Fn() + Send + Sync + 'static>>>,
|
||||
#[cfg(not(feature = "std"))]
|
||||
process_msgs_callback: RwLock<Option<Box<dyn Fn() + 'static>>>,
|
||||
}
|
||||
|
||||
impl MessageQueue {
|
||||
pub(crate) fn new() -> Self {
|
||||
let queue = Mutex::new(VecDeque::new());
|
||||
let process_msgs_callback = RwLock::new(None);
|
||||
Self { queue, process_msgs_callback }
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub(crate) fn set_process_msgs_callback(&self, callback: impl Fn() + Send + Sync + 'static) {
|
||||
*self.process_msgs_callback.write().unwrap() = Some(Box::new(callback));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub(crate) fn set_process_msgs_callback(&self, callback: impl Fn() + 'static) {
|
||||
*self.process_msgs_callback.write().unwrap() = Some(Box::new(callback));
|
||||
}
|
||||
|
||||
pub(crate) fn get_and_clear_pending_msgs(&self) -> Vec<(PublicKey, LSPSMessage)> {
|
||||
self.queue.lock().unwrap().drain(..).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn enqueue(&self, counterparty_node_id: &PublicKey, msg: LSPSMessage) {
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
queue.push_back((*counterparty_node_id, msg));
|
||||
}
|
||||
|
||||
if let Some(process_msgs_callback) = self.process_msgs_callback.read().unwrap().as_ref() {
|
||||
(process_msgs_callback)()
|
||||
}
|
||||
}
|
||||
}
|
1
lightning-liquidity/src/sync/debug_sync.rs
Symbolic link
1
lightning-liquidity/src/sync/debug_sync.rs
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../lightning/src/sync/debug_sync.rs
|
1
lightning-liquidity/src/sync/fairrwlock.rs
Symbolic link
1
lightning-liquidity/src/sync/fairrwlock.rs
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../lightning/src/sync/fairrwlock.rs
|
1
lightning-liquidity/src/sync/mod.rs
Symbolic link
1
lightning-liquidity/src/sync/mod.rs
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../lightning/src/sync/mod.rs
|
1
lightning-liquidity/src/sync/nostd_sync.rs
Symbolic link
1
lightning-liquidity/src/sync/nostd_sync.rs
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../lightning/src/sync/nostd_sync.rs
|
1
lightning-liquidity/src/sync/test_lockorder_checks.rs
Symbolic link
1
lightning-liquidity/src/sync/test_lockorder_checks.rs
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../lightning/src/sync/test_lockorder_checks.rs
|
1
lightning-liquidity/src/tests/mod.rs
Normal file
1
lightning-liquidity/src/tests/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod utils;
|
58
lightning-liquidity/src/tests/utils.rs
Normal file
58
lightning-liquidity/src/tests/utils.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use crate::prelude::Vec;
|
||||
use bitcoin::secp256k1::PublicKey;
|
||||
use lightning::io;
|
||||
use lightning::sign::EntropySource;
|
||||
|
||||
pub struct TestEntropy {}
|
||||
impl EntropySource for TestEntropy {
|
||||
fn get_secure_random_bytes(&self) -> [u8; 32] {
|
||||
[0; 32]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_vec(hex: &str) -> Option<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(hex.len() / 2);
|
||||
|
||||
let mut b = 0;
|
||||
for (idx, c) in hex.as_bytes().iter().enumerate() {
|
||||
b <<= 4;
|
||||
match *c {
|
||||
b'A'..=b'F' => b |= c - b'A' + 10,
|
||||
b'a'..=b'f' => b |= c - b'a' + 10,
|
||||
b'0'..=b'9' => b |= c - b'0',
|
||||
_ => return None,
|
||||
}
|
||||
if (idx & 1) == 1 {
|
||||
out.push(b);
|
||||
b = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
pub fn to_compressed_pubkey(hex: &str) -> Option<PublicKey> {
|
||||
if hex.len() != 33 * 2 {
|
||||
return None;
|
||||
}
|
||||
let data = match to_vec(&hex[0..33 * 2]) {
|
||||
Some(bytes) => bytes,
|
||||
None => return None,
|
||||
};
|
||||
match PublicKey::from_slice(&data) {
|
||||
Ok(pk) => Some(pk),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_pubkey(pubkey_str: &str) -> Result<PublicKey, io::Error> {
|
||||
let pubkey = to_compressed_pubkey(pubkey_str);
|
||||
if pubkey.is_none() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"ERROR: unable to parse given pubkey for node",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(pubkey.unwrap())
|
||||
}
|
53
lightning-liquidity/src/utils.rs
Normal file
53
lightning-liquidity/src/utils.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use core::{fmt::Write, ops::Deref};
|
||||
use lightning::sign::EntropySource;
|
||||
|
||||
use crate::lsps0::ser::RequestId;
|
||||
use crate::prelude::String;
|
||||
|
||||
pub fn scid_from_human_readable_string(human_readable_scid: &str) -> Result<u64, ()> {
|
||||
let mut parts = human_readable_scid.split('x');
|
||||
|
||||
let block: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?;
|
||||
let tx_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?;
|
||||
let vout_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?;
|
||||
|
||||
Ok((block << 40) | (tx_index << 16) | vout_index)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_request_id<ES: Deref>(entropy_source: &ES) -> RequestId
|
||||
where
|
||||
ES::Target: EntropySource,
|
||||
{
|
||||
let bytes = entropy_source.get_secure_random_bytes();
|
||||
RequestId(hex_str(&bytes[0..16]))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn hex_str(value: &[u8]) -> String {
|
||||
let mut res = String::with_capacity(2 * value.len());
|
||||
for v in value {
|
||||
write!(&mut res, "{:02x}", v).expect("Unable to write");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightning::util::scid_utils::{block_from_scid, tx_index_from_scid, vout_from_scid};
|
||||
|
||||
#[test]
|
||||
fn parses_human_readable_scid_correctly() {
|
||||
let block = 140;
|
||||
let tx_index = 123;
|
||||
let vout = 22;
|
||||
|
||||
let human_readable_scid = format!("{}x{}x{}", block, tx_index, vout);
|
||||
|
||||
let scid = scid_from_human_readable_string(&human_readable_scid).unwrap();
|
||||
|
||||
assert_eq!(block_from_scid(scid), block);
|
||||
assert_eq!(tx_index_from_scid(scid), tx_index);
|
||||
assert_eq!(vout_from_scid(scid), vout);
|
||||
}
|
||||
}
|
685
lightning-liquidity/tests/common/mod.rs
Normal file
685
lightning-liquidity/tests/common/mod.rs
Normal file
|
@ -0,0 +1,685 @@
|
|||
#![cfg(test)]
|
||||
// TODO: remove these flags and unused code once we know what we'll need.
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_macros)]
|
||||
|
||||
use lightning::chain::Filter;
|
||||
use lightning::sign::EntropySource;
|
||||
|
||||
use bitcoin::blockdata::constants::{genesis_block, ChainHash};
|
||||
use bitcoin::blockdata::transaction::Transaction;
|
||||
use bitcoin::Network;
|
||||
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
|
||||
use lightning::chain::{chainmonitor, BestBlock, Confirm};
|
||||
use lightning::ln::channelmanager;
|
||||
use lightning::ln::channelmanager::ChainParameters;
|
||||
use lightning::ln::functional_test_utils::*;
|
||||
use lightning::ln::msgs::{ChannelMessageHandler, Init};
|
||||
use lightning::ln::peer_handler::{
|
||||
IgnoringMessageHandler, MessageHandler, PeerManager, SocketDescriptor,
|
||||
};
|
||||
|
||||
use lightning::onion_message::messenger::DefaultMessageRouter;
|
||||
use lightning::routing::gossip::{NetworkGraph, P2PGossipSync};
|
||||
use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path};
|
||||
use lightning::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate};
|
||||
use lightning::sign::{InMemorySigner, KeysManager};
|
||||
use lightning::util::config::UserConfig;
|
||||
use lightning::util::persist::{
|
||||
KVStore, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
|
||||
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_KEY,
|
||||
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
|
||||
SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
|
||||
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
|
||||
};
|
||||
use lightning::util::test_utils;
|
||||
use lightning_liquidity::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig};
|
||||
use lightning_persister::fs_store::FilesystemStore;
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{env, fs};
|
||||
|
||||
pub(crate) struct TestEntropy {}
|
||||
impl EntropySource for TestEntropy {
|
||||
fn get_secure_random_bytes(&self) -> [u8; 32] {
|
||||
[0; 32]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct TestDescriptor {}
|
||||
impl SocketDescriptor for TestDescriptor {
|
||||
fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn disconnect_socket(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(c_bindings)]
|
||||
type LockingWrapper<T> = lightning::routing::scoring::MultiThreadedLockableScore<T>;
|
||||
#[cfg(not(c_bindings))]
|
||||
type LockingWrapper<T> = std::sync::Mutex<T>;
|
||||
|
||||
type ChannelManager = channelmanager::ChannelManager<
|
||||
Arc<ChainMonitor>,
|
||||
Arc<test_utils::TestBroadcaster>,
|
||||
Arc<KeysManager>,
|
||||
Arc<KeysManager>,
|
||||
Arc<KeysManager>,
|
||||
Arc<test_utils::TestFeeEstimator>,
|
||||
Arc<
|
||||
DefaultRouter<
|
||||
Arc<NetworkGraph<Arc<test_utils::TestLogger>>>,
|
||||
Arc<test_utils::TestLogger>,
|
||||
Arc<KeysManager>,
|
||||
Arc<LockingWrapper<TestScorer>>,
|
||||
(),
|
||||
TestScorer,
|
||||
>,
|
||||
>,
|
||||
Arc<
|
||||
DefaultMessageRouter<
|
||||
Arc<NetworkGraph<Arc<test_utils::TestLogger>>>,
|
||||
Arc<test_utils::TestLogger>,
|
||||
Arc<KeysManager>,
|
||||
>,
|
||||
>,
|
||||
Arc<test_utils::TestLogger>,
|
||||
>;
|
||||
|
||||
type ChainMonitor = chainmonitor::ChainMonitor<
|
||||
InMemorySigner,
|
||||
Arc<test_utils::TestChainSource>,
|
||||
Arc<test_utils::TestBroadcaster>,
|
||||
Arc<test_utils::TestFeeEstimator>,
|
||||
Arc<test_utils::TestLogger>,
|
||||
Arc<FilesystemStore>,
|
||||
>;
|
||||
|
||||
type PGS = Arc<
|
||||
P2PGossipSync<
|
||||
Arc<NetworkGraph<Arc<test_utils::TestLogger>>>,
|
||||
Arc<test_utils::TestChainSource>,
|
||||
Arc<test_utils::TestLogger>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub(crate) struct Node {
|
||||
pub(crate) channel_manager: Arc<ChannelManager>,
|
||||
pub(crate) keys_manager: Arc<KeysManager>,
|
||||
pub(crate) p2p_gossip_sync: PGS,
|
||||
pub(crate) peer_manager: Arc<
|
||||
PeerManager<
|
||||
TestDescriptor,
|
||||
Arc<test_utils::TestChannelMessageHandler>,
|
||||
Arc<test_utils::TestRoutingMessageHandler>,
|
||||
IgnoringMessageHandler,
|
||||
Arc<test_utils::TestLogger>,
|
||||
Arc<
|
||||
LiquidityManager<
|
||||
Arc<KeysManager>,
|
||||
Arc<ChannelManager>,
|
||||
Arc<dyn Filter + Send + Sync>,
|
||||
>,
|
||||
>,
|
||||
Arc<KeysManager>,
|
||||
>,
|
||||
>,
|
||||
pub(crate) liquidity_manager:
|
||||
Arc<LiquidityManager<Arc<KeysManager>, Arc<ChannelManager>, Arc<dyn Filter + Send + Sync>>>,
|
||||
pub(crate) check_msgs_processed: Arc<AtomicBool>,
|
||||
pub(crate) chain_monitor: Arc<ChainMonitor>,
|
||||
pub(crate) kv_store: Arc<FilesystemStore>,
|
||||
pub(crate) tx_broadcaster: Arc<test_utils::TestBroadcaster>,
|
||||
pub(crate) network_graph: Arc<NetworkGraph<Arc<test_utils::TestLogger>>>,
|
||||
pub(crate) logger: Arc<test_utils::TestLogger>,
|
||||
pub(crate) best_block: BestBlock,
|
||||
pub(crate) scorer: Arc<LockingWrapper<TestScorer>>,
|
||||
}
|
||||
|
||||
impl Drop for Node {
|
||||
fn drop(&mut self) {
|
||||
let data_dir = self.kv_store.get_data_dir();
|
||||
match fs::remove_dir_all(data_dir.clone()) {
|
||||
Err(e) => {
|
||||
println!("Failed to remove test store directory {}: {}", data_dir.display(), e)
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Persister {
|
||||
graph_error: Option<(lightning::io::ErrorKind, &'static str)>,
|
||||
graph_persistence_notifier: Option<SyncSender<()>>,
|
||||
manager_error: Option<(lightning::io::ErrorKind, &'static str)>,
|
||||
scorer_error: Option<(lightning::io::ErrorKind, &'static str)>,
|
||||
kv_store: FilesystemStore,
|
||||
}
|
||||
|
||||
impl Persister {
|
||||
fn new(data_dir: PathBuf) -> Self {
|
||||
let kv_store = FilesystemStore::new(data_dir);
|
||||
Self {
|
||||
graph_error: None,
|
||||
graph_persistence_notifier: None,
|
||||
manager_error: None,
|
||||
scorer_error: None,
|
||||
kv_store,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_graph_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self {
|
||||
Self { graph_error: Some((error, message)), ..self }
|
||||
}
|
||||
|
||||
fn with_graph_persistence_notifier(self, sender: SyncSender<()>) -> Self {
|
||||
Self { graph_persistence_notifier: Some(sender), ..self }
|
||||
}
|
||||
|
||||
fn with_manager_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self {
|
||||
Self { manager_error: Some((error, message)), ..self }
|
||||
}
|
||||
|
||||
fn with_scorer_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self {
|
||||
Self { scorer_error: Some((error, message)), ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl KVStore for Persister {
|
||||
fn read(
|
||||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
|
||||
) -> lightning::io::Result<Vec<u8>> {
|
||||
self.kv_store.read(primary_namespace, secondary_namespace, key)
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8],
|
||||
) -> lightning::io::Result<()> {
|
||||
if primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
|
||||
&& secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE
|
||||
&& key == CHANNEL_MANAGER_PERSISTENCE_KEY
|
||||
{
|
||||
if let Some((error, message)) = self.manager_error {
|
||||
return Err(lightning::io::Error::new(error, message));
|
||||
}
|
||||
}
|
||||
|
||||
if primary_namespace == NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE
|
||||
&& secondary_namespace == NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE
|
||||
&& key == NETWORK_GRAPH_PERSISTENCE_KEY
|
||||
{
|
||||
if let Some(sender) = &self.graph_persistence_notifier {
|
||||
match sender.send(()) {
|
||||
Ok(()) => {},
|
||||
Err(std::sync::mpsc::SendError(())) => {
|
||||
println!("Persister failed to notify as receiver went away.")
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((error, message)) = self.graph_error {
|
||||
return Err(lightning::io::Error::new(error, message));
|
||||
}
|
||||
}
|
||||
|
||||
if primary_namespace == SCORER_PERSISTENCE_PRIMARY_NAMESPACE
|
||||
&& secondary_namespace == SCORER_PERSISTENCE_SECONDARY_NAMESPACE
|
||||
&& key == SCORER_PERSISTENCE_KEY
|
||||
{
|
||||
if let Some((error, message)) = self.scorer_error {
|
||||
return Err(lightning::io::Error::new(error, message));
|
||||
}
|
||||
}
|
||||
|
||||
self.kv_store.write(primary_namespace, secondary_namespace, key, buf)
|
||||
}
|
||||
|
||||
fn remove(
|
||||
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
|
||||
) -> lightning::io::Result<()> {
|
||||
self.kv_store.remove(primary_namespace, secondary_namespace, key, lazy)
|
||||
}
|
||||
|
||||
fn list(
|
||||
&self, primary_namespace: &str, secondary_namespace: &str,
|
||||
) -> lightning::io::Result<Vec<String>> {
|
||||
self.kv_store.list(primary_namespace, secondary_namespace)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestScorer {
|
||||
event_expectations: Option<VecDeque<TestResult>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TestResult {
|
||||
PaymentFailure { path: Path, short_channel_id: u64 },
|
||||
PaymentSuccess { path: Path },
|
||||
ProbeFailure { path: Path },
|
||||
ProbeSuccess { path: Path },
|
||||
}
|
||||
|
||||
impl TestScorer {
|
||||
fn new() -> Self {
|
||||
Self { event_expectations: None }
|
||||
}
|
||||
|
||||
fn expect(&mut self, expectation: TestResult) {
|
||||
self.event_expectations.get_or_insert_with(VecDeque::new).push_back(expectation);
|
||||
}
|
||||
}
|
||||
|
||||
impl lightning::util::ser::Writeable for TestScorer {
|
||||
fn write<W: lightning::util::ser::Writer>(
|
||||
&self, _: &mut W,
|
||||
) -> Result<(), lightning::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ScoreLookUp for TestScorer {
|
||||
type ScoreParams = ();
|
||||
fn channel_penalty_msat(
|
||||
&self, _candidate: &CandidateRouteHop, _usage: ChannelUsage,
|
||||
_score_params: &Self::ScoreParams,
|
||||
) -> u64 {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
impl ScoreUpdate for TestScorer {
|
||||
fn payment_path_failed(
|
||||
&mut self, actual_path: &Path, actual_short_channel_id: u64, _: Duration,
|
||||
) {
|
||||
if let Some(expectations) = &mut self.event_expectations {
|
||||
match expectations.pop_front().unwrap() {
|
||||
TestResult::PaymentFailure { path, short_channel_id } => {
|
||||
assert_eq!(actual_path, &path);
|
||||
assert_eq!(actual_short_channel_id, short_channel_id);
|
||||
},
|
||||
TestResult::PaymentSuccess { path } => {
|
||||
panic!("Unexpected successful payment path: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeFailure { path } => {
|
||||
panic!("Unexpected probe failure: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeSuccess { path } => {
|
||||
panic!("Unexpected probe success: {:?}", path)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn payment_path_successful(&mut self, actual_path: &Path, _: Duration) {
|
||||
if let Some(expectations) = &mut self.event_expectations {
|
||||
match expectations.pop_front().unwrap() {
|
||||
TestResult::PaymentFailure { path, .. } => {
|
||||
panic!("Unexpected payment path failure: {:?}", path)
|
||||
},
|
||||
TestResult::PaymentSuccess { path } => {
|
||||
assert_eq!(actual_path, &path);
|
||||
},
|
||||
TestResult::ProbeFailure { path } => {
|
||||
panic!("Unexpected probe failure: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeSuccess { path } => {
|
||||
panic!("Unexpected probe success: {:?}", path)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_failed(&mut self, actual_path: &Path, _: u64, _: Duration) {
|
||||
if let Some(expectations) = &mut self.event_expectations {
|
||||
match expectations.pop_front().unwrap() {
|
||||
TestResult::PaymentFailure { path, .. } => {
|
||||
panic!("Unexpected payment path failure: {:?}", path)
|
||||
},
|
||||
TestResult::PaymentSuccess { path } => {
|
||||
panic!("Unexpected payment path success: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeFailure { path } => {
|
||||
assert_eq!(actual_path, &path);
|
||||
},
|
||||
TestResult::ProbeSuccess { path } => {
|
||||
panic!("Unexpected probe success: {:?}", path)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
fn probe_successful(&mut self, actual_path: &Path, _: Duration) {
|
||||
if let Some(expectations) = &mut self.event_expectations {
|
||||
match expectations.pop_front().unwrap() {
|
||||
TestResult::PaymentFailure { path, .. } => {
|
||||
panic!("Unexpected payment path failure: {:?}", path)
|
||||
},
|
||||
TestResult::PaymentSuccess { path } => {
|
||||
panic!("Unexpected payment path success: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeFailure { path } => {
|
||||
panic!("Unexpected probe failure: {:?}", path)
|
||||
},
|
||||
TestResult::ProbeSuccess { path } => {
|
||||
assert_eq!(actual_path, &path);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
fn time_passed(&mut self, _: Duration) {}
|
||||
}
|
||||
|
||||
#[cfg(c_bindings)]
|
||||
impl lightning::routing::scoring::Score for TestScorer {}
|
||||
|
||||
impl Drop for TestScorer {
|
||||
fn drop(&mut self) {
|
||||
if std::thread::panicking() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(event_expectations) = &self.event_expectations {
|
||||
if !event_expectations.is_empty() {
|
||||
panic!("Unsatisfied event expectations: {:?}", event_expectations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_full_filepath(filepath: String, filename: String) -> String {
|
||||
let mut path = PathBuf::from(filepath);
|
||||
path.push(filename);
|
||||
path.to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn create_liquidity_node(
|
||||
i: usize, persist_dir: &str, network: Network, service_config: Option<LiquidityServiceConfig>,
|
||||
client_config: Option<LiquidityClientConfig>,
|
||||
) -> Node {
|
||||
let tx_broadcaster = Arc::new(test_utils::TestBroadcaster::new(network));
|
||||
let fee_estimator = Arc::new(test_utils::TestFeeEstimator::new(253));
|
||||
let logger = Arc::new(test_utils::TestLogger::with_id(format!("node {}", i)));
|
||||
let genesis_block = genesis_block(network);
|
||||
let network_graph = Arc::new(NetworkGraph::new(network, logger.clone()));
|
||||
let scorer = Arc::new(LockingWrapper::new(TestScorer::new()));
|
||||
let now = Duration::from_secs(genesis_block.header.time as u64);
|
||||
let seed = [i as u8; 32];
|
||||
let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos()));
|
||||
let router = Arc::new(DefaultRouter::new(
|
||||
Arc::clone(&network_graph),
|
||||
logger.clone(),
|
||||
keys_manager.clone(),
|
||||
scorer.clone(),
|
||||
Default::default(),
|
||||
));
|
||||
let msg_router =
|
||||
Arc::new(DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager)));
|
||||
let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin));
|
||||
let kv_store =
|
||||
Arc::new(FilesystemStore::new(format!("{}_persister_{}", &persist_dir, i).into()));
|
||||
let chain_monitor = Arc::new(chainmonitor::ChainMonitor::new(
|
||||
Some(chain_source.clone()),
|
||||
tx_broadcaster.clone(),
|
||||
logger.clone(),
|
||||
fee_estimator.clone(),
|
||||
kv_store.clone(),
|
||||
));
|
||||
let best_block = BestBlock::from_network(network);
|
||||
let chain_params = ChainParameters { network, best_block };
|
||||
let channel_manager = Arc::new(ChannelManager::new(
|
||||
fee_estimator.clone(),
|
||||
chain_monitor.clone(),
|
||||
tx_broadcaster.clone(),
|
||||
router.clone(),
|
||||
msg_router.clone(),
|
||||
logger.clone(),
|
||||
keys_manager.clone(),
|
||||
keys_manager.clone(),
|
||||
keys_manager.clone(),
|
||||
UserConfig::default(),
|
||||
chain_params,
|
||||
genesis_block.header.time,
|
||||
));
|
||||
let p2p_gossip_sync = Arc::new(P2PGossipSync::new(
|
||||
network_graph.clone(),
|
||||
Some(chain_source.clone()),
|
||||
logger.clone(),
|
||||
));
|
||||
|
||||
let liquidity_manager = Arc::new(LiquidityManager::new(
|
||||
Arc::clone(&keys_manager),
|
||||
Arc::clone(&channel_manager),
|
||||
None::<Arc<dyn Filter + Send + Sync>>,
|
||||
Some(chain_params),
|
||||
service_config,
|
||||
client_config,
|
||||
));
|
||||
let msg_handler = MessageHandler {
|
||||
chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new(
|
||||
ChainHash::using_genesis_block(Network::Testnet),
|
||||
)),
|
||||
route_handler: Arc::new(test_utils::TestRoutingMessageHandler::new()),
|
||||
onion_message_handler: IgnoringMessageHandler {},
|
||||
custom_message_handler: Arc::clone(&liquidity_manager),
|
||||
};
|
||||
let peer_manager =
|
||||
Arc::new(PeerManager::new(msg_handler, 0, &seed, logger.clone(), keys_manager.clone()));
|
||||
|
||||
// Rather than registering PeerManager's process_events, we handle messages manually and use a
|
||||
// bool to check whether PeerManager would have been called as expected.
|
||||
let check_msgs_processed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let process_msgs_flag = Arc::clone(&check_msgs_processed);
|
||||
let process_msgs_callback = move || process_msgs_flag.store(true, Ordering::Release);
|
||||
liquidity_manager.set_process_msgs_callback(process_msgs_callback);
|
||||
|
||||
Node {
|
||||
channel_manager,
|
||||
keys_manager,
|
||||
p2p_gossip_sync,
|
||||
peer_manager,
|
||||
liquidity_manager,
|
||||
check_msgs_processed,
|
||||
chain_monitor,
|
||||
kv_store,
|
||||
tx_broadcaster,
|
||||
network_graph,
|
||||
logger,
|
||||
best_block,
|
||||
scorer,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_service_and_client_nodes(
|
||||
persist_dir: &str, service_config: LiquidityServiceConfig, client_config: LiquidityClientConfig,
|
||||
) -> (Node, Node) {
|
||||
let persist_temp_path = env::temp_dir().join(persist_dir);
|
||||
let persist_dir = persist_temp_path.to_string_lossy().to_string();
|
||||
let network = Network::Bitcoin;
|
||||
|
||||
let service_node = create_liquidity_node(1, &persist_dir, network, Some(service_config), None);
|
||||
let client_node = create_liquidity_node(2, &persist_dir, network, None, Some(client_config));
|
||||
|
||||
service_node
|
||||
.channel_manager
|
||||
.peer_connected(
|
||||
client_node.channel_manager.get_our_node_id(),
|
||||
&Init {
|
||||
features: client_node.channel_manager.init_features(),
|
||||
networks: None,
|
||||
remote_network_address: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
client_node
|
||||
.channel_manager
|
||||
.peer_connected(
|
||||
service_node.channel_manager.get_our_node_id(),
|
||||
&Init {
|
||||
features: service_node.channel_manager.init_features(),
|
||||
networks: None,
|
||||
remote_network_address: None,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(service_node, client_node)
|
||||
}
|
||||
|
||||
macro_rules! open_channel {
|
||||
($node_a: expr, $node_b: expr, $channel_value: expr) => {{
|
||||
begin_open_channel!($node_a, $node_b, $channel_value);
|
||||
let events = $node_a.node.get_and_clear_pending_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
let (temporary_channel_id, tx) =
|
||||
handle_funding_generation_ready!(events[0], $channel_value);
|
||||
$node_a
|
||||
.node
|
||||
.funding_transaction_generated(
|
||||
&temporary_channel_id,
|
||||
&$node_b.node.get_our_node_id(),
|
||||
tx.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
$node_b.node.handle_funding_created(
|
||||
&$node_a.node.get_our_node_id(),
|
||||
&get_event_msg!(
|
||||
$node_a,
|
||||
MessageSendEvent::SendFundingCreated,
|
||||
$node_b.node.get_our_node_id()
|
||||
),
|
||||
);
|
||||
get_event!($node_b, Event::ChannelPending);
|
||||
$node_a.node.handle_funding_signed(
|
||||
&$node_b.node.get_our_node_id(),
|
||||
&get_event_msg!(
|
||||
$node_b,
|
||||
MessageSendEvent::SendFundingSigned,
|
||||
$node_a.node.get_our_node_id()
|
||||
),
|
||||
);
|
||||
get_event!($node_a, Event::ChannelPending);
|
||||
tx
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use open_channel;
|
||||
|
||||
macro_rules! begin_open_channel {
|
||||
($node_a: expr, $node_b: expr, $channel_value: expr) => {{
|
||||
$node_a
|
||||
.node
|
||||
.create_channel($node_b.node.get_our_node_id(), $channel_value, 100, 42, None, None)
|
||||
.unwrap();
|
||||
$node_b.node.handle_open_channel(
|
||||
&$node_a.node.get_our_node_id(),
|
||||
&get_event_msg!(
|
||||
$node_a,
|
||||
MessageSendEvent::SendOpenChannel,
|
||||
$node_b.node.get_our_node_id()
|
||||
),
|
||||
);
|
||||
$node_a.node.handle_accept_channel(
|
||||
&$node_b.node.get_our_node_id(),
|
||||
&get_event_msg!(
|
||||
$node_b,
|
||||
MessageSendEvent::SendAcceptChannel,
|
||||
$node_a.node.get_our_node_id()
|
||||
),
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use begin_open_channel;
|
||||
|
||||
macro_rules! handle_funding_generation_ready {
|
||||
($event: expr, $channel_value: expr) => {{
|
||||
match $event {
|
||||
Event::FundingGenerationReady {
|
||||
temporary_channel_id,
|
||||
channel_value_satoshis,
|
||||
ref output_script,
|
||||
user_channel_id,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(channel_value_satoshis, $channel_value);
|
||||
assert_eq!(user_channel_id, 42);
|
||||
|
||||
let tx = Transaction {
|
||||
version: 1 as i32,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: Vec::new(),
|
||||
output: vec![TxOut {
|
||||
value: channel_value_satoshis,
|
||||
script_pubkey: output_script.clone(),
|
||||
}],
|
||||
};
|
||||
(temporary_channel_id, tx)
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use handle_funding_generation_ready;
|
||||
|
||||
macro_rules! get_lsps_message {
|
||||
($node: expr, $expected_target_node_id: expr) => {{
|
||||
use std::sync::atomic::Ordering;
|
||||
assert!($node.check_msgs_processed.swap(false, Ordering::AcqRel));
|
||||
let msgs = $node.liquidity_manager.get_and_clear_pending_msg();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let (target_node_id, message) = msgs.into_iter().next().unwrap();
|
||||
assert_eq!(target_node_id, $expected_target_node_id);
|
||||
message
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use get_lsps_message;
|
||||
|
||||
fn confirm_transaction_depth(node: &mut Node, tx: &Transaction, depth: u32) {
|
||||
for i in 1..=depth {
|
||||
let prev_blockhash = node.best_block.block_hash;
|
||||
let height = node.best_block.height + 1;
|
||||
let header = create_dummy_header(prev_blockhash, height);
|
||||
let txdata = vec![(0, tx)];
|
||||
node.best_block = BestBlock::new(header.block_hash(), height);
|
||||
match i {
|
||||
1 => {
|
||||
node.channel_manager.transactions_confirmed(&header, &txdata, height);
|
||||
node.chain_monitor.transactions_confirmed(&header, &txdata, height);
|
||||
},
|
||||
x if x == depth => {
|
||||
node.channel_manager.best_block_updated(&header, height);
|
||||
node.chain_monitor.best_block_updated(&header, height);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_transaction(node: &mut Node, tx: &Transaction) {
|
||||
confirm_transaction_depth(node, tx, ANTI_REORG_DELAY);
|
||||
}
|
||||
|
||||
fn advance_chain(node: &mut Node, num_blocks: u32) {
|
||||
for i in 1..=num_blocks {
|
||||
let prev_blockhash = node.best_block.block_hash;
|
||||
let height = node.best_block.height + 1;
|
||||
let header = create_dummy_header(prev_blockhash, height);
|
||||
node.best_block = BestBlock::new(header.block_hash(), height);
|
||||
if i == num_blocks {
|
||||
node.channel_manager.best_block_updated(&header, height);
|
||||
node.chain_monitor.best_block_updated(&header, height);
|
||||
}
|
||||
}
|
||||
}
|
241
lightning-liquidity/tests/lsps2_integration_tests.rs
Normal file
241
lightning-liquidity/tests/lsps2_integration_tests.rs
Normal file
|
@ -0,0 +1,241 @@
|
|||
#![cfg(all(test, feature = "std"))]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{create_service_and_client_nodes, get_lsps_message, Node};
|
||||
|
||||
use lightning_liquidity::events::Event;
|
||||
use lightning_liquidity::lsps2::client::LSPS2ClientConfig;
|
||||
use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent};
|
||||
use lightning_liquidity::lsps2::msgs::RawOpeningFeeParams;
|
||||
use lightning_liquidity::lsps2::service::LSPS2ServiceConfig;
|
||||
use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params;
|
||||
use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig};
|
||||
|
||||
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA;
|
||||
use lightning::ln::peer_handler::CustomMessageHandler;
|
||||
use lightning::log_error;
|
||||
use lightning::routing::router::{RouteHint, RouteHintHop};
|
||||
use lightning::util::logger::Logger;
|
||||
|
||||
use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees};
|
||||
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::secp256k1::{PublicKey, Secp256k1};
|
||||
use bitcoin::Network;
|
||||
|
||||
use chrono::DateTime;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
fn create_jit_invoice(
|
||||
node: &Node, service_node_id: PublicKey, intercept_scid: u64, cltv_expiry_delta: u32,
|
||||
payment_size_msat: Option<u64>, description: &str, expiry_secs: u32,
|
||||
) -> Result<Bolt11Invoice, ()> {
|
||||
// LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual.
|
||||
let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2;
|
||||
let (payment_hash, payment_secret) = node
|
||||
.channel_manager
|
||||
.create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta))
|
||||
.map_err(|e| {
|
||||
log_error!(node.logger, "Failed to register inbound payment: {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
let route_hint = RouteHint(vec![RouteHintHop {
|
||||
src_node_id: service_node_id,
|
||||
short_channel_id: intercept_scid,
|
||||
fees: RoutingFees { base_msat: 0, proportional_millionths: 0 },
|
||||
cltv_expiry_delta: cltv_expiry_delta as u16,
|
||||
htlc_minimum_msat: None,
|
||||
htlc_maximum_msat: None,
|
||||
}]);
|
||||
|
||||
let payment_hash = sha256::Hash::from_slice(&payment_hash.0).map_err(|e| {
|
||||
log_error!(node.logger, "Invalid payment hash: {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
let currency = Network::Bitcoin.into();
|
||||
let mut invoice_builder = InvoiceBuilder::new(currency)
|
||||
.description(description.to_string())
|
||||
.payment_hash(payment_hash)
|
||||
.payment_secret(payment_secret)
|
||||
.current_timestamp()
|
||||
.min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into())
|
||||
.expiry_time(Duration::from_secs(expiry_secs.into()))
|
||||
.private_route(route_hint);
|
||||
|
||||
if let Some(amount_msat) = payment_size_msat {
|
||||
invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp();
|
||||
}
|
||||
|
||||
invoice_builder
|
||||
.build_signed(|hash| {
|
||||
Secp256k1::new().sign_ecdsa_recoverable(hash, &node.keys_manager.get_node_secret_key())
|
||||
})
|
||||
.map_err(|e| {
|
||||
log_error!(node.logger, "Failed to build and sign invoice: {}", e);
|
||||
()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invoice_generation_flow() {
|
||||
let promise_secret = [42; 32];
|
||||
let lsps2_service_config = LSPS2ServiceConfig { promise_secret };
|
||||
let service_config = LiquidityServiceConfig {
|
||||
#[cfg(lsps1_service)]
|
||||
lsps1_service_config: None,
|
||||
lsps2_service_config: Some(lsps2_service_config),
|
||||
advertise_service: true,
|
||||
};
|
||||
|
||||
let lsps2_client_config = LSPS2ClientConfig::default();
|
||||
let client_config = LiquidityClientConfig {
|
||||
lsps1_client_config: None,
|
||||
lsps2_client_config: Some(lsps2_client_config),
|
||||
};
|
||||
|
||||
let (service_node, client_node) =
|
||||
create_service_and_client_nodes("invoice_generation_flow", service_config, client_config);
|
||||
|
||||
let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap();
|
||||
let service_node_id = service_node.channel_manager.get_our_node_id();
|
||||
|
||||
let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap();
|
||||
let client_node_id = client_node.channel_manager.get_our_node_id();
|
||||
|
||||
let get_info_request_id = client_handler.request_opening_params(service_node_id, None);
|
||||
let get_info_request = get_lsps_message!(client_node, service_node_id);
|
||||
|
||||
service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap();
|
||||
|
||||
let get_info_event = service_node.liquidity_manager.next_event().unwrap();
|
||||
match get_info_event {
|
||||
Event::LSPS2Service(LSPS2ServiceEvent::GetInfo {
|
||||
request_id,
|
||||
counterparty_node_id,
|
||||
token,
|
||||
}) => {
|
||||
assert_eq!(request_id, get_info_request_id);
|
||||
assert_eq!(counterparty_node_id, client_node_id);
|
||||
assert_eq!(token, None);
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
}
|
||||
|
||||
let raw_opening_params = RawOpeningFeeParams {
|
||||
min_fee_msat: 100,
|
||||
proportional: 21,
|
||||
valid_until: DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into(),
|
||||
min_lifetime: 144,
|
||||
max_client_to_self_delay: 128,
|
||||
min_payment_size_msat: 1,
|
||||
max_payment_size_msat: 100_000_000,
|
||||
};
|
||||
|
||||
service_handler
|
||||
.opening_fee_params_generated(
|
||||
&client_node_id,
|
||||
get_info_request_id.clone(),
|
||||
vec![raw_opening_params],
|
||||
)
|
||||
.unwrap();
|
||||
let get_info_response = get_lsps_message!(service_node, client_node_id);
|
||||
|
||||
client_node
|
||||
.liquidity_manager
|
||||
.handle_custom_message(get_info_response, service_node_id)
|
||||
.unwrap();
|
||||
|
||||
let opening_params_event = client_node.liquidity_manager.next_event().unwrap();
|
||||
let opening_fee_params = match opening_params_event {
|
||||
Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady {
|
||||
request_id,
|
||||
counterparty_node_id,
|
||||
opening_fee_params_menu,
|
||||
}) => {
|
||||
assert_eq!(request_id, get_info_request_id);
|
||||
assert_eq!(counterparty_node_id, service_node_id);
|
||||
let opening_fee_params = opening_fee_params_menu.first().unwrap().clone();
|
||||
assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret));
|
||||
opening_fee_params
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
};
|
||||
|
||||
let payment_size_msat = Some(1_000_000);
|
||||
let buy_request_id = client_handler
|
||||
.select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone())
|
||||
.unwrap();
|
||||
|
||||
let buy_request = get_lsps_message!(client_node, service_node_id);
|
||||
service_node.liquidity_manager.handle_custom_message(buy_request, client_node_id).unwrap();
|
||||
|
||||
let buy_event = service_node.liquidity_manager.next_event().unwrap();
|
||||
match buy_event {
|
||||
Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest {
|
||||
request_id,
|
||||
counterparty_node_id,
|
||||
opening_fee_params: ofp,
|
||||
payment_size_msat: psm,
|
||||
}) => {
|
||||
assert_eq!(request_id, buy_request_id);
|
||||
assert_eq!(counterparty_node_id, client_node_id);
|
||||
assert_eq!(opening_fee_params, ofp);
|
||||
assert_eq!(payment_size_msat, psm);
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
}
|
||||
|
||||
let user_channel_id = 42;
|
||||
let cltv_expiry_delta = 144;
|
||||
let intercept_scid = service_node.channel_manager.get_intercept_scid();
|
||||
let client_trusts_lsp = true;
|
||||
|
||||
service_handler
|
||||
.invoice_parameters_generated(
|
||||
&client_node_id,
|
||||
buy_request_id.clone(),
|
||||
intercept_scid,
|
||||
cltv_expiry_delta,
|
||||
client_trusts_lsp,
|
||||
user_channel_id,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let buy_response = get_lsps_message!(service_node, client_node_id);
|
||||
client_node.liquidity_manager.handle_custom_message(buy_response, service_node_id).unwrap();
|
||||
|
||||
let invoice_params_event = client_node.liquidity_manager.next_event().unwrap();
|
||||
match invoice_params_event {
|
||||
Event::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady {
|
||||
request_id,
|
||||
counterparty_node_id,
|
||||
intercept_scid: iscid,
|
||||
cltv_expiry_delta: ced,
|
||||
payment_size_msat: psm,
|
||||
}) => {
|
||||
assert_eq!(request_id, buy_request_id);
|
||||
assert_eq!(counterparty_node_id, service_node_id);
|
||||
assert_eq!(intercept_scid, iscid);
|
||||
assert_eq!(cltv_expiry_delta, ced);
|
||||
assert_eq!(payment_size_msat, psm);
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
};
|
||||
|
||||
let description = "asdf";
|
||||
let expiry_secs = 3600;
|
||||
let _invoice = create_jit_invoice(
|
||||
&client_node,
|
||||
service_node_id,
|
||||
intercept_scid,
|
||||
cltv_expiry_delta,
|
||||
payment_size_msat,
|
||||
description,
|
||||
expiry_secs,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub use alloc::sync::Arc;
|
||||
use core::fmt;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use core::time::Duration;
|
||||
|
||||
|
@ -65,6 +66,11 @@ impl Condvar {
|
|||
pub fn notify_all(&self) {
|
||||
self.inner.notify_all();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn notify_one(&self) {
|
||||
self.inner.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
@ -254,6 +260,21 @@ impl<T: Sized> Mutex<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Sized + fmt::Debug> fmt::Debug for Mutex<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut d = f.debug_struct("Mutex");
|
||||
match self.try_lock() {
|
||||
Ok(guard) => {
|
||||
d.field("data", &&*guard);
|
||||
},
|
||||
Err(()) => {
|
||||
d.field("data", &format_args!("<locked>"));
|
||||
},
|
||||
}
|
||||
d.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "if unused the Mutex will immediately unlock"]
|
||||
pub struct MutexGuard<'a, T: Sized + 'a> {
|
||||
mutex: &'a Mutex<T>,
|
||||
|
|
|
@ -5,6 +5,7 @@ use core::ops::{Deref, DerefMut};
|
|||
|
||||
pub type LockResult<Guard> = Result<Guard, ()>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mutex<T: ?Sized> {
|
||||
inner: RefCell<T>,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//!
|
||||
//! This module simply re-exports the `HashMap` used in LDK for public consumption.
|
||||
|
||||
pub(crate) use hashbrown::hash_map;
|
||||
pub use hashbrown::hash_map;
|
||||
|
||||
mod hashbrown_tables {
|
||||
#[cfg(feature = "std")]
|
||||
|
@ -67,7 +67,8 @@ mod hashbrown_tables {
|
|||
|
||||
/// The HashMap type used in LDK.
|
||||
pub type HashMap<K, V> = hashbrown::HashMap<K, V, RandomState>;
|
||||
pub(crate) type HashSet<K> = hashbrown::HashSet<K, RandomState>;
|
||||
/// The HashSet type used in LDK.
|
||||
pub type HashSet<K> = hashbrown::HashSet<K, RandomState>;
|
||||
|
||||
pub(crate) type OccupiedHashMapEntry<'a, K, V> =
|
||||
hashbrown::hash_map::OccupiedEntry<'a, K, V, RandomState>;
|
||||
|
@ -96,9 +97,11 @@ mod hashbrown_tables {
|
|||
res
|
||||
}
|
||||
|
||||
pub(crate) fn new_hash_set<K>() -> HashSet<K> {
|
||||
/// Builds a new [`HashSet`].
|
||||
pub fn new_hash_set<K>() -> HashSet<K> {
|
||||
HashSet::with_hasher(RandomState::new())
|
||||
}
|
||||
/// Builds a new [`HashSet`] with the given capacity.
|
||||
pub(crate) fn hash_set_with_capacity<K>(cap: usize) -> HashSet<K> {
|
||||
HashSet::with_capacity_and_hasher(cap, RandomState::new())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue