mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-20 06:05:09 +01:00
feat: cln lightning
This commit is contained in:
13
Cargo.toml
13
Cargo.toml
@@ -22,24 +22,23 @@ keywords = ["bitcoin", "e-cash", "cashu"]
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.74"
|
||||
anyhow = "1"
|
||||
bitcoin = { version = "0.30", default-features = false } # lightning-invoice uses v0.30
|
||||
bip39 = "2.0"
|
||||
cdk = { version = "0.1", path = "./crates/cdk", default-features = false }
|
||||
cdk-rexie = { version = "0.1", path = "./crates/cdk-rexie", default-features = false }
|
||||
cdk-sqlite = { version = "0.1", path = "./crates/cdk-sqlite", default-features = false }
|
||||
cdk-redb = { version = "0.1", path = "./crates/cdk-redb", default-features = false }
|
||||
cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde-wasm-bindgen = { version = "0.6.5", default-features = false }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
futures = { version = "0.3.28", default-feature = false }
|
||||
web-sys = { version = "0.3.69", default-features = false, features = ["console"] }
|
||||
bitcoin = { version = "0.30", features = [
|
||||
"serde",
|
||||
"rand",
|
||||
"rand-std",
|
||||
] } # lightning-invoice uses v0.30
|
||||
anyhow = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[profile]
|
||||
|
||||
|
||||
20
crates/cdk-cln/Cargo.toml
Normal file
20
crates/cdk-cln/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "cdk-cln"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["CDK Developers"]
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true # MSRV
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
bitcoin.workspace = true
|
||||
cdk = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cln-rpc = "0.1.9"
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
25
crates/cdk-cln/src/error.rs
Normal file
25
crates/cdk-cln/src/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Wrong CLN response
|
||||
#[error("Wrong cln response")]
|
||||
WrongClnResponse,
|
||||
/// Unknown invoice
|
||||
#[error("Unknown invoice")]
|
||||
UnknownInvoice,
|
||||
/// Cln Error
|
||||
#[error(transparent)]
|
||||
Cln(#[from] cln_rpc::Error),
|
||||
/// Cln Rpc Error
|
||||
#[error(transparent)]
|
||||
ClnRpc(#[from] cln_rpc::RpcError),
|
||||
#[error("`{0}`")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_lightning::Error {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::Lightning(Box::new(e))
|
||||
}
|
||||
}
|
||||
256
crates/cdk-cln/src/lib.rs
Normal file
256
crates/cdk-cln/src/lib.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! CDK lightning backend for CLN
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cdk::cdk_lightning::{self, MintLightning, PayInvoiceResponse};
|
||||
use cdk::nuts::{MeltQuoteState, MintQuoteState};
|
||||
use cdk::util::{hex, unix_time};
|
||||
use cdk::Bolt11Invoice;
|
||||
use cln_rpc::model::requests::{
|
||||
InvoiceRequest, ListinvoicesRequest, PayRequest, WaitanyinvoiceRequest,
|
||||
};
|
||||
use cln_rpc::model::responses::{ListinvoicesInvoicesStatus, PayStatus, WaitanyinvoiceResponse};
|
||||
use cln_rpc::model::Request;
|
||||
use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
|
||||
use error::Error;
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cln {
|
||||
rpc_socket: PathBuf,
|
||||
cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
|
||||
}
|
||||
|
||||
impl Cln {
|
||||
pub async fn new(rpc_socket: PathBuf) -> Result<Self, Error> {
|
||||
let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
|
||||
|
||||
Ok(Self {
|
||||
rpc_socket,
|
||||
cln_client: Arc::new(Mutex::new(cln_client)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MintLightning for Cln {
|
||||
type Err = cdk_lightning::Error;
|
||||
|
||||
async fn wait_any_invoice(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err> {
|
||||
let last_pay_index = self.get_last_pay_index().await?;
|
||||
let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
||||
|
||||
Ok(futures::stream::unfold(
|
||||
(cln_client, last_pay_index),
|
||||
|(mut cln_client, mut last_pay_idx)| async move {
|
||||
loop {
|
||||
let invoice_res = cln_client
|
||||
.call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest {
|
||||
timeout: None,
|
||||
lastpay_index: last_pay_idx,
|
||||
}))
|
||||
.await;
|
||||
|
||||
let invoice: WaitanyinvoiceResponse = match invoice_res {
|
||||
Ok(invoice) => invoice,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error fetching invoice: {e}");
|
||||
// Let's not spam CLN with requests on failure
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// Retry same request
|
||||
continue;
|
||||
}
|
||||
}
|
||||
.try_into()
|
||||
.expect("Wrong response from CLN");
|
||||
|
||||
last_pay_idx = invoice.pay_index;
|
||||
|
||||
if let Some(bolt11) = invoice.bolt11 {
|
||||
if let Ok(invoice) = Bolt11Invoice::from_str(&bolt11) {
|
||||
break Some((invoice, (cln_client, last_pay_idx)));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed())
|
||||
}
|
||||
|
||||
async fn pay_invoice(
|
||||
&self,
|
||||
bolt11: Bolt11Invoice,
|
||||
partial_msats: Option<u64>,
|
||||
max_fee_msats: Option<u64>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
let cln_response = cln_client
|
||||
.call(Request::Pay(PayRequest {
|
||||
bolt11: bolt11.to_string(),
|
||||
amount_msat: None,
|
||||
label: None,
|
||||
riskfactor: None,
|
||||
maxfeepercent: None,
|
||||
retry_for: None,
|
||||
maxdelay: None,
|
||||
exemptfee: None,
|
||||
localinvreqid: None,
|
||||
exclude: None,
|
||||
maxfee: max_fee_msats.map(CLN_Amount::from_msat),
|
||||
description: None,
|
||||
partial_msat: partial_msats.map(CLN_Amount::from_msat),
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let response = match cln_response {
|
||||
cln_rpc::Response::Pay(pay_response) => {
|
||||
let status = match pay_response.status {
|
||||
PayStatus::COMPLETE => MeltQuoteState::Paid,
|
||||
PayStatus::PENDING => MeltQuoteState::Pending,
|
||||
PayStatus::FAILED => MeltQuoteState::Unpaid,
|
||||
};
|
||||
PayInvoiceResponse {
|
||||
payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
|
||||
payment_hash: pay_response.payment_hash.to_string(),
|
||||
status,
|
||||
total_spent_msats: pay_response.amount_sent_msat.msat(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn create_invoice(
|
||||
&self,
|
||||
amount_msats: u64,
|
||||
description: String,
|
||||
unix_expiry: u64,
|
||||
) -> Result<Bolt11Invoice, Self::Err> {
|
||||
let time_now = unix_time();
|
||||
assert!(unix_expiry > time_now);
|
||||
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
|
||||
let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount_msats));
|
||||
let cln_response = cln_client
|
||||
.call(cln_rpc::Request::Invoice(InvoiceRequest {
|
||||
amount_msat,
|
||||
description,
|
||||
label: Uuid::new_v4().to_string(),
|
||||
expiry: Some(unix_expiry - time_now),
|
||||
fallbacks: None,
|
||||
preimage: None,
|
||||
cltv: None,
|
||||
deschashonly: None,
|
||||
exposeprivatechannels: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let invoice = match cln_response {
|
||||
cln_rpc::Response::Invoice(invoice_res) => {
|
||||
Bolt11Invoice::from_str(&invoice_res.bolt11)?
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
return Err(Error::WrongClnResponse.into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(invoice)
|
||||
}
|
||||
|
||||
async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
|
||||
let cln_response = cln_client
|
||||
.call(Request::ListInvoices(ListinvoicesRequest {
|
||||
payment_hash: Some(payment_hash.to_string()),
|
||||
label: None,
|
||||
invstring: None,
|
||||
offer_id: None,
|
||||
index: None,
|
||||
limit: None,
|
||||
start: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let status = match cln_response {
|
||||
cln_rpc::Response::ListInvoices(invoice_response) => {
|
||||
match invoice_response.invoices.first() {
|
||||
Some(invoice_response) => {
|
||||
cln_invoice_status_to_mint_state(invoice_response.status)
|
||||
}
|
||||
None => {
|
||||
tracing::info!(
|
||||
"Check invoice called on unknown payment_hash: {}",
|
||||
payment_hash
|
||||
);
|
||||
return Err(Error::WrongClnResponse.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
return Err(Error::Custom("CLN returned wrong response kind".to_string()).into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl Cln {
|
||||
async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
|
||||
let mut cln_client = self.cln_client.lock().await;
|
||||
let cln_response = cln_client
|
||||
.call(cln_rpc::Request::ListInvoices(ListinvoicesRequest {
|
||||
index: None,
|
||||
invstring: None,
|
||||
label: None,
|
||||
limit: None,
|
||||
offer_id: None,
|
||||
payment_hash: None,
|
||||
start: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
match cln_response {
|
||||
cln_rpc::Response::ListInvoices(invoice_res) => match invoice_res.invoices.last() {
|
||||
Some(last_invoice) => Ok(last_invoice.pay_index),
|
||||
None => Ok(None),
|
||||
},
|
||||
_ => {
|
||||
tracing::warn!("CLN returned wrong response kind");
|
||||
Err(Error::WrongClnResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState {
|
||||
match status {
|
||||
ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid,
|
||||
ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid,
|
||||
ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,13 @@ license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["mint", "wallet"]
|
||||
mint = []
|
||||
mint = ["dep:futures"]
|
||||
wallet = ["dep:reqwest"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
base64 = "0.22" # bitcoin uses v0.13 (optional dep)
|
||||
bitcoin = { workspace = true, features = [
|
||||
"serde",
|
||||
@@ -38,8 +39,9 @@ serde_json.workspace = true
|
||||
serde_with = "3.4"
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
futures = { workspace = true, optional = true }
|
||||
url = "2.3"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
uuid.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { workspace = true, features = [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! CDK Amount
|
||||
//!
|
||||
//! Is any and will be treated as the unit of the wallet
|
||||
//! Is any unit and will be treated as the unit of the wallet
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Number of satoshis
|
||||
/// Amount can be any unit
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Amount(u64);
|
||||
@@ -68,18 +68,6 @@ impl Amount {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kinds of targeting that are supported
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum SplitTarget {
|
||||
/// Default target; least amount of proofs
|
||||
#[default]
|
||||
None,
|
||||
/// Target amount for wallet to have most proofs that add up to value
|
||||
Value(Amount),
|
||||
}
|
||||
|
||||
impl Default for Amount {
|
||||
fn default() -> Self {
|
||||
Amount::ZERO
|
||||
@@ -173,6 +161,18 @@ impl core::iter::Sum for Amount {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kinds of targeting that are supported
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum SplitTarget {
|
||||
/// Default target; least amount of proofs
|
||||
#[default]
|
||||
None,
|
||||
/// Target amount for wallet to have most proofs that add up to value
|
||||
Value(Amount),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
72
crates/cdk/src/cdk_lightning/mod.rs
Normal file
72
crates/cdk/src/cdk_lightning/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! CDK Mint Lightning
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::nuts::{MeltQuoteState, MintQuoteState};
|
||||
|
||||
/// CDK Lightning Error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Lightning Error
|
||||
#[error(transparent)]
|
||||
Lightning(Box<dyn std::error::Error + Send + Sync>),
|
||||
/// Serde Error
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde_json::Error),
|
||||
/// AnyHow Error
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
/// Parse Error
|
||||
#[error(transparent)]
|
||||
Parse(#[from] ParseOrSemanticError),
|
||||
}
|
||||
|
||||
/// MintLighting Trait
|
||||
#[async_trait]
|
||||
pub trait MintLightning {
|
||||
/// Mint Lightning Error
|
||||
type Err: Into<Error> + From<Error>;
|
||||
|
||||
/// Create a new invoice
|
||||
async fn create_invoice(
|
||||
&self,
|
||||
msats: u64,
|
||||
description: String,
|
||||
unix_expiry: u64,
|
||||
) -> Result<Bolt11Invoice, Self::Err>;
|
||||
|
||||
/// Pay bolt11 invoice
|
||||
async fn pay_invoice(
|
||||
&self,
|
||||
bolt11: Bolt11Invoice,
|
||||
partial_msats: Option<u64>,
|
||||
max_fee_msats: Option<u64>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err>;
|
||||
|
||||
/// Listen for invoices to be paid to the mint
|
||||
async fn wait_any_invoice(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Bolt11Invoice> + Send>>, Self::Err>;
|
||||
|
||||
/// Check the status of an incoming payment
|
||||
async fn check_invoice_status(&self, payment_hash: &str) -> Result<MintQuoteState, Self::Err>;
|
||||
}
|
||||
|
||||
/// Pay invoice response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PayInvoiceResponse {
|
||||
/// Payment hash
|
||||
pub payment_hash: String,
|
||||
/// Payment Preimage
|
||||
pub payment_preimage: Option<String>,
|
||||
/// Status
|
||||
pub status: MeltQuoteState,
|
||||
/// Totoal Amount Spent in msats
|
||||
pub total_spent_msats: u64,
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
pub mod amount;
|
||||
pub mod cdk_database;
|
||||
#[cfg(feature = "mint")]
|
||||
pub mod cdk_lightning;
|
||||
pub mod dhke;
|
||||
pub mod error;
|
||||
#[cfg(feature = "mint")]
|
||||
|
||||
@@ -32,6 +32,7 @@ buildargs=(
|
||||
"-p cdk-redb --no-default-features --features mint"
|
||||
"-p cdk-sqlite --no-default-features --features mint"
|
||||
"-p cdk-sqlite --no-default-features --features wallet"
|
||||
"-p cdk-cln"
|
||||
"--bin cdk-cli"
|
||||
"--examples"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user