Explorar el Código

feat: Mint fake wallet ln backend

thesimplekid hace 7 meses
padre
commit
e0efd316d1

+ 1 - 0
Cargo.toml

@@ -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 - 0
crates/cdk-fake-wallet/Cargo.toml

@@ -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 - 0
crates/cdk-fake-wallet/src/error.rs

@@ -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 - 0
crates/cdk-fake-wallet/src/lib.rs

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

+ 1 - 0
crates/cdk-mintd/Cargo.toml

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

+ 3 - 2
crates/cdk-mintd/src/config.rs

@@ -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 - 17
crates/cdk-mintd/src/main.rs

@@ -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"))?;
-
-                Arc::new(Cln::new(cln_socket, fee_reserve, 1000, 1000000, 1000, 100000).await?)
-            }
-        };
+    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?)
+        }
+        LnBackend::FakeWallet => Arc::new(FakeWallet::new(fee_reserve, 1000, 1000000, 1000, 10000)),
+    };
 
     let mut ln_backends = HashMap::new();
 

+ 1 - 0
misc/scripts/check-crates.sh

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