feat: Mint fake wallet ln backend

This commit is contained in:
thesimplekid
2024-07-16 16:54:03 +01:00
parent 4f240f3953
commit e0efd316d1
8 changed files with 272 additions and 18 deletions

View File

@@ -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"] }

View 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"

View 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))
}
}

View 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)
}
}

View File

@@ -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"] }

View File

@@ -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)

View File

@@ -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();

View File

@@ -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"