mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-23 05:56:02 +01:00
feat: Mint fake wallet ln backend
This commit is contained in:
@@ -31,6 +31,7 @@ cdk-sqlite = { version = "0.2", path = "./crates/cdk-sqlite", default-features =
|
||||
cdk-redb = { version = "0.2", path = "./crates/cdk-redb", default-features = false }
|
||||
cdk-cln = { version = "0.1", path = "./crates/cdk-cln", default-features = false }
|
||||
cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = false }
|
||||
cdk-fake-wallet = { version = "0.1", path = "./crates/cdk-fake-wallet", default-features = false }
|
||||
tokio = { version = "1", default-features = false }
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
|
||||
|
||||
24
crates/cdk-fake-wallet/Cargo.toml
Normal file
24
crates/cdk-fake-wallet/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "cdk-fake-wallet"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["CDK Developers"]
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true # MSRV
|
||||
license.workspace = true
|
||||
description = "CDK ln backend for cln"
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
bitcoin.workspace = true
|
||||
cdk = { workspace = true, default-features = false, features = ["mint"] }
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
lightning-invoice.workspace = true
|
||||
lightning = "0.0.123"
|
||||
tokio-stream = "0.1.15"
|
||||
rand = "0.8.5"
|
||||
19
crates/cdk-fake-wallet/src/error.rs
Normal file
19
crates/cdk-fake-wallet/src/error.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Invoice amount not defined
|
||||
#[error("Unknown invoice amount")]
|
||||
UnknownInvoiceAmount,
|
||||
/// Unknown invoice
|
||||
#[error("Unknown invoice")]
|
||||
UnknownInvoice,
|
||||
#[error("`{0}`")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<Error> for cdk::cdk_lightning::Error {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::Lightning(Box::new(e))
|
||||
}
|
||||
}
|
||||
203
crates/cdk-fake-wallet/src/lib.rs
Normal file
203
crates/cdk-fake-wallet/src/lib.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! CDK lightning backend for CLN
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::secp256k1::{Secp256k1, SecretKey};
|
||||
use cdk::cdk_lightning::{
|
||||
self, to_unit, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse,
|
||||
Settings,
|
||||
};
|
||||
use cdk::mint;
|
||||
use cdk::mint::FeeReserve;
|
||||
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
|
||||
use cdk::util::unix_time;
|
||||
use error::Error;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::Stream;
|
||||
use lightning::ln::types::PaymentSecret;
|
||||
use lightning_invoice::{Currency, InvoiceBuilder};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FakeWallet {
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
mint_enabled: bool,
|
||||
melt_enabled: bool,
|
||||
sender: tokio::sync::mpsc::Sender<String>,
|
||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
||||
}
|
||||
|
||||
impl FakeWallet {
|
||||
pub fn new(
|
||||
fee_reserve: FeeReserve,
|
||||
min_melt_amount: u64,
|
||||
max_melt_amount: u64,
|
||||
min_mint_amount: u64,
|
||||
max_mint_amount: u64,
|
||||
) -> Self {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(8);
|
||||
|
||||
Self {
|
||||
fee_reserve,
|
||||
min_mint_amount,
|
||||
max_mint_amount,
|
||||
min_melt_amount,
|
||||
max_melt_amount,
|
||||
mint_enabled: true,
|
||||
melt_enabled: true,
|
||||
sender,
|
||||
receiver: Arc::new(Mutex::new(Some(receiver))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MintLightning for FakeWallet {
|
||||
type Err = cdk_lightning::Error;
|
||||
|
||||
fn get_settings(&self) -> Settings {
|
||||
Settings {
|
||||
mpp: true,
|
||||
min_mint_amount: self.min_mint_amount,
|
||||
max_mint_amount: self.max_mint_amount,
|
||||
min_melt_amount: self.min_melt_amount,
|
||||
max_melt_amount: self.max_melt_amount,
|
||||
unit: CurrencyUnit::Msat,
|
||||
mint_enabled: self.mint_enabled,
|
||||
melt_enabled: self.melt_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_any_invoice(
|
||||
&self,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
||||
let receiver = self
|
||||
.receiver
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.ok_or(Error::Custom("No reeiver".to_string()))?;
|
||||
let receiver_stream = ReceiverStream::new(receiver);
|
||||
Ok(Box::pin(receiver_stream.map(|label| label)))
|
||||
}
|
||||
|
||||
async fn get_payment_quote(
|
||||
&self,
|
||||
melt_quote_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
|
||||
let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * amount as f32) as u64;
|
||||
|
||||
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
|
||||
|
||||
let fee = match relative_fee_reserve > absolute_fee_reserve {
|
||||
true => relative_fee_reserve,
|
||||
false => absolute_fee_reserve,
|
||||
};
|
||||
|
||||
Ok(PaymentQuoteResponse {
|
||||
request_lookup_id: melt_quote_request.request.payment_hash().to_string(),
|
||||
amount,
|
||||
fee,
|
||||
})
|
||||
}
|
||||
|
||||
async fn pay_invoice(
|
||||
&self,
|
||||
melt_quote: mint::MeltQuote,
|
||||
_partial_msats: Option<u64>,
|
||||
_max_fee_msats: Option<u64>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
Ok(PayInvoiceResponse {
|
||||
payment_preimage: Some("".to_string()),
|
||||
payment_hash: "".to_string(),
|
||||
status: MeltQuoteState::Paid,
|
||||
total_spent_msats: melt_quote.amount.into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_invoice(
|
||||
&self,
|
||||
amount_msats: u64,
|
||||
description: String,
|
||||
unix_expiry: u64,
|
||||
) -> Result<CreateInvoiceResponse, Self::Err> {
|
||||
let time_now = unix_time();
|
||||
assert!(unix_expiry > time_now);
|
||||
|
||||
let label = Uuid::new_v4().to_string();
|
||||
|
||||
let private_key = SecretKey::from_slice(
|
||||
&[
|
||||
0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2,
|
||||
0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca,
|
||||
0x3b, 0x2d, 0xb7, 0x34,
|
||||
][..],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap();
|
||||
let payment_secret = PaymentSecret([42u8; 32]);
|
||||
|
||||
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
|
||||
.description(description)
|
||||
.payment_hash(payment_hash)
|
||||
.payment_secret(payment_secret)
|
||||
.amount_milli_satoshis(amount_msats)
|
||||
.current_timestamp()
|
||||
.min_final_cltv_expiry_delta(144)
|
||||
.build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
|
||||
.unwrap();
|
||||
|
||||
// Create a random delay between 3 and 6 seconds
|
||||
let duration = time::Duration::from_secs(3)
|
||||
+ time::Duration::from_millis(rand::random::<u64>() % 3001);
|
||||
|
||||
let sender = self.sender.clone();
|
||||
let label_clone = label.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Wait for the random delay to elapse
|
||||
time::sleep(duration).await;
|
||||
|
||||
// Send the message after waiting for the specified duration
|
||||
if sender.send(label_clone.clone()).await.is_err() {
|
||||
tracing::error!("Failed to send label: {}", label_clone);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(CreateInvoiceResponse {
|
||||
request_lookup_id: label,
|
||||
request: invoice,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_invoice_status(
|
||||
&self,
|
||||
_request_lookup_id: &str,
|
||||
) -> Result<MintQuoteState, Self::Err> {
|
||||
Ok(MintQuoteState::Paid)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ cdk = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cdk-redb = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cdk-sqlite = { workspace = true, default-features = false, features = ["mint"] }
|
||||
cdk-cln = { workspace = true, default-features = false }
|
||||
cdk-fake-wallet = { workspace = true, default-features = false }
|
||||
cdk-axum = { workspace = true, default-features = false }
|
||||
config = { version = "0.13.3", features = ["toml"] }
|
||||
clap = { version = "4.4.8", features = ["derive", "env", "default"] }
|
||||
|
||||
@@ -20,8 +20,8 @@ pub struct Info {
|
||||
pub enum LnBackend {
|
||||
#[default]
|
||||
Cln,
|
||||
// Greenlight,
|
||||
// Ldk,
|
||||
FakeWallet, // Greenlight,
|
||||
// Ldk,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -114,6 +114,7 @@ impl Settings {
|
||||
LnBackend::Cln => assert!(settings.ln.cln_path.is_some()),
|
||||
//LnBackend::Greenlight => (),
|
||||
//LnBackend::Ldk => (),
|
||||
LnBackend::FakeWallet => (),
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
|
||||
@@ -21,6 +21,7 @@ use cdk::nuts::{
|
||||
use cdk::{cdk_lightning, Amount};
|
||||
use cdk_axum::LnKey;
|
||||
use cdk_cln::Cln;
|
||||
use cdk_fake_wallet::FakeWallet;
|
||||
use cdk_redb::MintRedbDatabase;
|
||||
use cdk_sqlite::MintSqliteDatabase;
|
||||
use clap::Parser;
|
||||
@@ -116,23 +117,26 @@ async fn main() -> anyhow::Result<()> {
|
||||
min_fee_reserve: absolute_ln_fee_reserve,
|
||||
percent_fee_reserve: relative_ln_fee,
|
||||
};
|
||||
let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> =
|
||||
match settings.ln.ln_backend {
|
||||
LnBackend::Cln => {
|
||||
let cln_socket = expand_path(
|
||||
settings
|
||||
.ln
|
||||
.cln_path
|
||||
.clone()
|
||||
.ok_or(anyhow!("cln socket not defined"))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("cln socket not defined"))?,
|
||||
)
|
||||
.ok_or(anyhow!("cln socket not defined"))?;
|
||||
let ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync> = match settings
|
||||
.ln
|
||||
.ln_backend
|
||||
{
|
||||
LnBackend::Cln => {
|
||||
let cln_socket = expand_path(
|
||||
settings
|
||||
.ln
|
||||
.cln_path
|
||||
.clone()
|
||||
.ok_or(anyhow!("cln socket not defined"))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("cln socket not defined"))?,
|
||||
)
|
||||
.ok_or(anyhow!("cln socket not defined"))?;
|
||||
|
||||
Arc::new(Cln::new(cln_socket, fee_reserve, 1000, 1000000, 1000, 100000).await?)
|
||||
}
|
||||
};
|
||||
Arc::new(Cln::new(cln_socket, fee_reserve, 1000, 1000000, 1000, 100000).await?)
|
||||
}
|
||||
LnBackend::FakeWallet => Arc::new(FakeWallet::new(fee_reserve, 1000, 1000000, 1000, 10000)),
|
||||
};
|
||||
|
||||
let mut ln_backends = HashMap::new();
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ buildargs=(
|
||||
"-p cdk-sqlite --no-default-features --features wallet"
|
||||
"-p cdk-cln"
|
||||
"-p cdk-axum"
|
||||
"-p cdk-fake-wallet"
|
||||
"--bin cdk-cli"
|
||||
"--bin cdk-mintd"
|
||||
"--examples"
|
||||
|
||||
Reference in New Issue
Block a user