From 162507c492d39167370a80088020a35d2681a47a Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Tue, 11 Feb 2025 13:36:43 +0000 Subject: [PATCH] feat: payment processor --- .github/workflows/ci.yml | 28 ++ Cargo.toml | 10 + crates/cashu/src/nuts/nut00/mod.rs | 9 +- crates/cdk-cln/Cargo.toml | 1 + crates/cdk-cln/src/error.rs | 2 +- crates/cdk-cln/src/lib.rs | 75 ++-- crates/cdk-common/src/common.rs | 13 +- crates/cdk-common/src/database/mint.rs | 6 +- crates/cdk-common/src/error.rs | 4 +- crates/cdk-common/src/lib.rs | 2 +- .../src/{lightning.rs => payment.rs} | 81 ++-- crates/cdk-fake-wallet/Cargo.toml | 2 +- crates/cdk-fake-wallet/src/error.rs | 2 +- crates/cdk-fake-wallet/src/lib.rs | 90 +++-- .../src/init_pure_tests.rs | 22 +- .../cdk-integration-tests/src/init_regtest.rs | 2 +- crates/cdk-integration-tests/tests/mint.rs | 18 +- .../tests/payment_processor.rs | 176 +++++++++ crates/cdk-lnbits/Cargo.toml | 1 + crates/cdk-lnbits/src/error.rs | 2 +- crates/cdk-lnbits/src/lib.rs | 89 +++-- crates/cdk-lnd/Cargo.toml | 1 + crates/cdk-lnd/src/error.rs | 2 +- crates/cdk-lnd/src/lib.rs | 106 +++--- crates/cdk-mint-rpc/Cargo.toml | 10 +- crates/cdk-mintd/Cargo.toml | 16 +- crates/cdk-mintd/example.config.toml | 7 + crates/cdk-mintd/src/config.rs | 24 +- .../cdk-mintd/src/env_vars/grpc_processor.rs | 44 +++ crates/cdk-mintd/src/env_vars/ln.rs | 2 + crates/cdk-mintd/src/env_vars/mod.rs | 9 + crates/cdk-mintd/src/main.rs | 139 +++++-- crates/cdk-mintd/src/setup.rs | 32 +- crates/cdk-payment-processor/Cargo.toml | 65 ++++ crates/cdk-payment-processor/README.md | 77 ++++ crates/cdk-payment-processor/build.rs | 5 + .../src/bin/payment_processor.rs | 206 +++++++++++ crates/cdk-payment-processor/src/error.rs | 20 + crates/cdk-payment-processor/src/lib.rs | 8 + .../cdk-payment-processor/src/proto/client.rs | 299 +++++++++++++++ crates/cdk-payment-processor/src/proto/mod.rs | 207 +++++++++++ .../src/proto/payment_processor.proto | 113 ++++++ .../cdk-payment-processor/src/proto/server.rs | 345 ++++++++++++++++++ crates/cdk-redb/src/mint/mod.rs | 6 +- crates/cdk-sqlite/src/mint/memory.rs | 4 +- crates/cdk-sqlite/src/mint/mod.rs | 12 +- crates/cdk/src/lib.rs | 2 +- crates/cdk/src/mint/builder.rs | 32 +- crates/cdk/src/mint/ln.rs | 6 +- crates/cdk/src/mint/melt.rs | 52 +-- crates/cdk/src/mint/mint_nut04.rs | 21 +- crates/cdk/src/mint/mod.rs | 37 +- crates/cdk/src/mint/start_up_check.rs | 6 +- crates/cdk/src/wallet/mod.rs | 2 +- justfile | 102 +++++- misc/mintd_payment_processor.sh | 154 ++++++++ misc/test.just | 9 - 57 files changed, 2460 insertions(+), 357 deletions(-) rename crates/cdk-common/src/{lightning.rs => payment.rs} (65%) create mode 100644 crates/cdk-integration-tests/tests/payment_processor.rs create mode 100644 crates/cdk-mintd/src/env_vars/grpc_processor.rs create mode 100644 crates/cdk-payment-processor/Cargo.toml create mode 100644 crates/cdk-payment-processor/README.md create mode 100644 crates/cdk-payment-processor/build.rs create mode 100644 crates/cdk-payment-processor/src/bin/payment_processor.rs create mode 100644 crates/cdk-payment-processor/src/error.rs create mode 100644 crates/cdk-payment-processor/src/lib.rs create mode 100644 crates/cdk-payment-processor/src/proto/client.rs create mode 100644 crates/cdk-payment-processor/src/proto/mod.rs create mode 100644 crates/cdk-payment-processor/src/proto/payment_processor.proto create mode 100644 crates/cdk-payment-processor/src/proto/server.rs create mode 100755 misc/mintd_payment_processor.sh delete mode 100644 misc/test.just diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e2c3a35..74d51949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,7 @@ jobs: -p cdk-lnd, -p cdk-lnbits, -p cdk-fake-wallet, + -p cdk-payment-processor, --bin cdk-cli, --bin cdk-cli --features sqlcipher, --bin cdk-mintd, @@ -115,9 +116,11 @@ jobs: --bin cdk-mintd --no-default-features --features cln, --bin cdk-mintd --no-default-features --features lnbits, --bin cdk-mintd --no-default-features --features fakewallet, + --bin cdk-mintd --no-default-features --features grpc-processor, --bin cdk-mintd --no-default-features --features "management-rpc lnd", --bin cdk-mintd --no-default-features --features "management-rpc cln", --bin cdk-mintd --no-default-features --features "management-rpc lnbits", + --bin cdk-mintd --no-default-features --features "management-rpc grpc-processor", --bin cdk-mintd --no-default-features --features "swagger lnd", --bin cdk-mintd --no-default-features --features "swagger cln", --bin cdk-mintd --no-default-features --features "swagger lnbits", @@ -211,6 +214,30 @@ jobs: - name: Test fake mint run: nix develop -i -L .#stable --command just test + + payment-processor-itests: + name: "Payment processor tests" + runs-on: ubuntu-latest + strategy: + matrix: + ln: + [ + FAKEWALLET, + CLN, + LND + ] + steps: + - name: checkout + uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v11 + - name: Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v6 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + - name: Test + run: nix develop -i -L .#stable --command just itest-payment-processor ${{matrix.ln}} + msrv-build: name: "MSRV build" runs-on: ubuntu-latest @@ -231,6 +258,7 @@ jobs: -p cdk-mint-rpc, -p cdk-sqlite, -p cdk-mintd, + -p cdk-payment-processor --no-default-features, ] steps: - name: checkout diff --git a/Cargo.toml b/Cargo.toml index b2b8e627..b03668bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ cdk-cln = { path = "./crates/cdk-cln", version = "=0.7.1" } cdk-lnbits = { path = "./crates/cdk-lnbits", version = "=0.7.1" } cdk-lnd = { path = "./crates/cdk-lnd", version = "=0.7.1" } cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.7.1" } +cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.7.1" } cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.7.1" } cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.7.1" } cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.7.1" } @@ -40,6 +41,7 @@ tokio = { version = "1", default-features = false, features = ["rt", "macros", " tokio-util = { version = "0.7.11", default-features = false } tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] } tokio-tungstenite = { version = "0.26.0", default-features = false } +tokio-stream = "0.1.15" tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = "2.3" @@ -63,6 +65,14 @@ once_cell = "1.20.2" instant = { version = "0.1", default-features = false } rand = "0.8.5" home = "0.5.5" +tonic = { version = "0.12.3", features = [ + "channel", + "tls", + "tls-webpki-roots", +] } +prost = "0.13.1" +tonic-build = "0.12" + [workspace.metadata] diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index f21a72c7..6490f908 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -455,20 +455,22 @@ impl<'de> Deserialize<'de> for CurrencyUnit { /// Payment Method #[non_exhaustive] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub enum PaymentMethod { /// Bolt11 payment type #[default] Bolt11, + /// Custom + Custom(String), } impl FromStr for PaymentMethod { type Err = Error; fn from_str(value: &str) -> Result { - match value { + match value.to_lowercase().as_str() { "bolt11" => Ok(Self::Bolt11), - _ => Err(Error::UnsupportedPaymentMethod), + c => Ok(Self::Custom(c.to_string())), } } } @@ -477,6 +479,7 @@ impl fmt::Display for PaymentMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PaymentMethod::Bolt11 => write!(f, "bolt11"), + PaymentMethod::Custom(p) => write!(f, "{}", p), } } } diff --git a/crates/cdk-cln/Cargo.toml b/crates/cdk-cln/Cargo.toml index 64064c00..7460b0e0 100644 --- a/crates/cdk-cln/Cargo.toml +++ b/crates/cdk-cln/Cargo.toml @@ -20,3 +20,4 @@ tokio-util.workspace = true tracing.workspace = true thiserror.workspace = true uuid.workspace = true +serde_json.workspace = true diff --git a/crates/cdk-cln/src/error.rs b/crates/cdk-cln/src/error.rs index e97832fc..cd6d8d1e 100644 --- a/crates/cdk-cln/src/error.rs +++ b/crates/cdk-cln/src/error.rs @@ -28,7 +28,7 @@ pub enum Error { Amount(#[from] cdk::amount::Error), } -impl From for cdk::cdk_lightning::Error { +impl From for cdk::cdk_payment::Error { fn from(e: Error) -> Self { Self::Lightning(Box::new(e)) } diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index bb4ab218..ab0db004 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -10,12 +10,13 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use async_trait::async_trait; -use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, +use cdk::amount::{to_unit, Amount}; +use cdk::cdk_payment::{ + self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment, + PaymentQuoteResponse, }; -use cdk::mint::FeeReserve; -use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; +use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState}; +use cdk::types::FeeReserve; use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; use cln_rpc::model::requests::{ @@ -28,6 +29,7 @@ use cln_rpc::model::responses::{ use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; use error::Error; use futures::{Stream, StreamExt}; +use serde_json::Value; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use uuid::Uuid; @@ -60,15 +62,15 @@ impl Cln { } #[async_trait] -impl MintLightning for Cln { - type Err = cdk_lightning::Error; +impl MintPayment for Cln { + type Err = cdk_payment::Error; - fn get_settings(&self) -> Settings { - Settings { + async fn get_settings(&self) -> Result { + Ok(serde_json::to_value(Bolt11Settings { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, - } + })?) } /// Is wait invoice active @@ -81,7 +83,7 @@ impl MintLightning for Cln { self.wait_invoice_cancel_token.cancel() } - async fn wait_any_invoice( + async fn wait_any_incoming_payment( &self, ) -> Result + Send>>, Self::Err> { let last_pay_index = self.get_last_pay_index().await?; @@ -175,11 +177,21 @@ impl MintLightning for Cln { async fn get_payment_quote( &self, - melt_quote_request: &MeltQuoteBolt11Request, + request: &str, + unit: &CurrencyUnit, + options: Option, ) -> Result { - let amount = melt_quote_request.amount_msat()?; + let bolt11 = Bolt11Invoice::from_str(request)?; - let amount = amount / MSAT_IN_SAT.into(); + let amount_msat = match options { + Some(amount) => amount.amount_msat(), + None => bolt11 + .amount_milli_satoshis() + .ok_or(Error::UnknownInvoiceAmount)? + .into(), + }; + + let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?; let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; @@ -192,19 +204,19 @@ impl MintLightning for Cln { }; Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), + request_lookup_id: bolt11.payment_hash().to_string(), amount, fee: fee.into(), state: MeltQuoteState::Unpaid, }) } - async fn pay_invoice( + async fn make_payment( &self, melt_quote: mint::MeltQuote, partial_amount: Option, max_fee: Option, - ) -> Result { + ) -> Result { let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; let pay_state = self .check_outgoing_payment(&bolt11.payment_hash().to_string()) @@ -271,8 +283,8 @@ impl MintLightning for Cln { PayStatus::FAILED => MeltQuoteState::Failed, }; - PayInvoiceResponse { - payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), + MakePaymentResponse { + payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())), payment_lookup_id: pay_response.payment_hash.to_string(), status, total_spent: to_unit( @@ -292,15 +304,14 @@ impl MintLightning for Cln { Ok(response) } - async fn create_invoice( + async fn create_incoming_payment_request( &self, amount: Amount, unit: &CurrencyUnit, description: String, - unix_expiry: u64, - ) -> Result { + unix_expiry: Option, + ) -> Result { let time_now = unix_time(); - assert!(unix_expiry > time_now); let mut cln_client = self.cln_client.lock().await; @@ -314,7 +325,7 @@ impl MintLightning for Cln { amount_msat, description, label: label.clone(), - expiry: Some(unix_expiry - time_now), + expiry: unix_expiry.map(|t| t - time_now), fallbacks: None, preimage: None, cltv: None, @@ -328,14 +339,14 @@ impl MintLightning for Cln { let expiry = request.expires_at().map(|t| t.as_secs()); let payment_hash = request.payment_hash(); - Ok(CreateInvoiceResponse { + Ok(CreateIncomingPaymentResponse { request_lookup_id: payment_hash.to_string(), - request, + request: request.to_string(), expiry, }) } - async fn check_incoming_invoice_status( + async fn check_incoming_payment_status( &self, payment_hash: &str, ) -> Result { @@ -371,7 +382,7 @@ impl MintLightning for Cln { async fn check_outgoing_payment( &self, payment_hash: &str, - ) -> Result { + ) -> Result { let mut cln_client = self.cln_client.lock().await; let listpays_response = cln_client @@ -390,9 +401,9 @@ impl MintLightning for Cln { Some(pays_response) => { let status = cln_pays_status_to_mint_state(pays_response.status); - Ok(PayInvoiceResponse { + Ok(MakePaymentResponse { payment_lookup_id: pays_response.payment_hash.to_string(), - payment_preimage: pays_response.preimage.map(|p| hex::encode(p.to_vec())), + payment_proof: pays_response.preimage.map(|p| hex::encode(p.to_vec())), status, total_spent: pays_response .amount_sent_msat @@ -400,9 +411,9 @@ impl MintLightning for Cln { unit: CurrencyUnit::Msat, }) } - None => Ok(PayInvoiceResponse { + None => Ok(MakePaymentResponse { payment_lookup_id: payment_hash.to_string(), - payment_preimage: None, + payment_proof: None, status: MeltQuoteState::Unknown, total_spent: Amount::ZERO, unit: CurrencyUnit::Msat, diff --git a/crates/cdk-common/src/common.rs b/crates/cdk-common/src/common.rs index b2fd8a4d..e8256112 100644 --- a/crates/cdk-common/src/common.rs +++ b/crates/cdk-common/src/common.rs @@ -143,14 +143,14 @@ impl ProofInfo { /// Key used in hashmap of ln backends to identify what unit and payment method /// it is for #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct LnKey { +pub struct PaymentProcessorKey { /// Unit of Payment backend pub unit: CurrencyUnit, /// Method of payment backend pub method: PaymentMethod, } -impl LnKey { +impl PaymentProcessorKey { /// Create new [`LnKey`] pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self { Self { unit, method } @@ -241,3 +241,12 @@ mod tests { assert_eq!(melted.total_amount(), Amount::from(32)); } } + +/// Mint Fee Reserve +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FeeReserve { + /// Absolute expected min fee + pub min_fee_reserve: Amount, + /// Percentage expected fee + pub percent_fee_reserve: f32, +} diff --git a/crates/cdk-common/src/database/mint.rs b/crates/cdk-common/src/database/mint.rs index 673f4c8c..a6cdd486 100644 --- a/crates/cdk-common/src/database/mint.rs +++ b/crates/cdk-common/src/database/mint.rs @@ -7,7 +7,7 @@ use cashu::MintInfo; use uuid::Uuid; use super::Error; -use crate::common::{LnKey, QuoteTTL}; +use crate::common::{PaymentProcessorKey, QuoteTTL}; use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote}; use crate::nuts::{ BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof, @@ -76,13 +76,13 @@ pub trait Database { async fn add_melt_request( &self, melt_request: MeltBolt11Request, - ln_key: LnKey, + ln_key: PaymentProcessorKey, ) -> Result<(), Self::Err>; /// Get melt request async fn get_melt_request( &self, quote_id: &Uuid, - ) -> Result, LnKey)>, Self::Err>; + ) -> Result, PaymentProcessorKey)>, Self::Err>; /// Add [`MintKeySetInfo`] async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>; diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index aec61fbe..97b00b54 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -264,10 +264,10 @@ pub enum Error { /// Database Error #[error(transparent)] Database(#[from] crate::database::Error), - /// Lightning Error + /// Payment Error #[error(transparent)] #[cfg(feature = "mint")] - Lightning(#[from] crate::lightning::Error), + Payment(#[from] crate::payment::Error), } /// CDK Error Response diff --git a/crates/cdk-common/src/lib.rs b/crates/cdk-common/src/lib.rs index a8c1f5ca..12b93463 100644 --- a/crates/cdk-common/src/lib.rs +++ b/crates/cdk-common/src/lib.rs @@ -10,7 +10,7 @@ pub mod common; pub mod database; pub mod error; #[cfg(feature = "mint")] -pub mod lightning; +pub mod payment; pub mod pub_sub; pub mod subscription; pub mod ws; diff --git a/crates/cdk-common/src/lightning.rs b/crates/cdk-common/src/payment.rs similarity index 65% rename from crates/cdk-common/src/lightning.rs rename to crates/cdk-common/src/payment.rs index 639882d9..1be2360e 100644 --- a/crates/cdk-common/src/lightning.rs +++ b/crates/cdk-common/src/payment.rs @@ -3,12 +3,14 @@ use std::pin::Pin; use async_trait::async_trait; +use cashu::MeltOptions; use futures::Stream; -use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError}; +use lightning_invoice::ParseOrSemanticError; use serde::{Deserialize, Serialize}; +use serde_json::Value; use thiserror::Error; -use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; +use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState}; use crate::{mint, Amount}; /// CDK Lightning Error @@ -23,6 +25,9 @@ pub enum Error { /// Unsupported unit #[error("Unsupported unit")] UnsupportedUnit, + /// Unsupported payment option + #[error("Unsupported payment option")] + UnsupportedPaymentOption, /// Payment state is unknown #[error("Payment state is unknown")] UnknownPaymentState, @@ -41,47 +46,55 @@ pub enum Error { /// Amount Error #[error(transparent)] Amount(#[from] crate::amount::Error), + /// NUT04 Error + #[error(transparent)] + NUT04(#[from] crate::nuts::nut04::Error), /// NUT05 Error #[error(transparent)] NUT05(#[from] crate::nuts::nut05::Error), + /// Custom + #[error("`{0}`")] + Custom(String), } -/// MintLighting Trait +/// Mint payment trait #[async_trait] -pub trait MintLightning { +pub trait MintPayment { /// Mint Lightning Error type Err: Into + From; - /// Base Unit - fn get_settings(&self) -> Settings; + /// Base Settings + async fn get_settings(&self) -> Result; /// Create a new invoice - async fn create_invoice( + async fn create_incoming_payment_request( &self, amount: Amount, unit: &CurrencyUnit, description: String, - unix_expiry: u64, - ) -> Result; + unix_expiry: Option, + ) -> Result; /// Get payment quote /// Used to get fee and amount required for a payment request async fn get_payment_quote( &self, - melt_quote_request: &MeltQuoteBolt11Request, + request: &str, + unit: &CurrencyUnit, + options: Option, ) -> Result; - /// Pay bolt11 invoice - async fn pay_invoice( + /// Pay request + async fn make_payment( &self, melt_quote: mint::MeltQuote, partial_amount: Option, max_fee_amount: Option, - ) -> Result; + ) -> Result; /// Listen for invoices to be paid to the mint /// Returns a stream of request_lookup_id once invoices are paid - async fn wait_any_invoice( + async fn wait_any_incoming_payment( &self, ) -> Result + Send>>, Self::Err>; @@ -92,7 +105,7 @@ pub trait MintLightning { fn cancel_wait_invoice(&self); /// Check the status of an incoming payment - async fn check_incoming_invoice_status( + async fn check_incoming_payment_status( &self, request_lookup_id: &str, ) -> Result; @@ -101,27 +114,27 @@ pub trait MintLightning { async fn check_outgoing_payment( &self, request_lookup_id: &str, - ) -> Result; + ) -> Result; } -/// Create invoice response +/// Create incoming payment response #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct CreateInvoiceResponse { - /// Id that is used to look up the invoice from the ln backend +pub struct CreateIncomingPaymentResponse { + /// Id that is used to look up the payment from the ln backend pub request_lookup_id: String, - /// Bolt11 payment request - pub request: Bolt11Invoice, + /// Payment request + pub request: String, /// Unix Expiry of Invoice pub expiry: Option, } -/// Pay invoice response +/// Payment response #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct PayInvoiceResponse { +pub struct MakePaymentResponse { /// Payment hash pub payment_lookup_id: String, - /// Payment Preimage - pub payment_preimage: Option, + /// Payment proof + pub payment_proof: Option, /// Status pub status: MeltQuoteState, /// Total Amount Spent @@ -145,7 +158,7 @@ pub struct PaymentQuoteResponse { /// Ln backend settings #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct Settings { +pub struct Bolt11Settings { /// MPP supported pub mpp: bool, /// Base unit of backend @@ -153,3 +166,19 @@ pub struct Settings { /// Invoice Description supported pub invoice_description: bool, } + +impl TryFrom for Value { + type Error = crate::error::Error; + + fn try_from(value: Bolt11Settings) -> Result { + serde_json::to_value(value).map_err(|err| err.into()) + } +} + +impl TryFrom for Bolt11Settings { + type Error = crate::error::Error; + + fn try_from(value: Value) -> Result { + serde_json::from_value(value).map_err(|err| err.into()) + } +} diff --git a/crates/cdk-fake-wallet/Cargo.toml b/crates/cdk-fake-wallet/Cargo.toml index 8807f131..6f12821a 100644 --- a/crates/cdk-fake-wallet/Cargo.toml +++ b/crates/cdk-fake-wallet/Cargo.toml @@ -21,4 +21,4 @@ thiserror.workspace = true serde.workspace = true serde_json.workspace = true lightning-invoice.workspace = true -tokio-stream = "0.1.15" +tokio-stream.workspace = true diff --git a/crates/cdk-fake-wallet/src/error.rs b/crates/cdk-fake-wallet/src/error.rs index 036d1cab..69ee0321 100644 --- a/crates/cdk-fake-wallet/src/error.rs +++ b/crates/cdk-fake-wallet/src/error.rs @@ -16,7 +16,7 @@ pub enum Error { NoReceiver, } -impl From for cdk::cdk_lightning::Error { +impl From for cdk::cdk_payment::Error { fn from(e: Error) -> Self { Self::Lightning(Box::new(e)) } diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 73ed4667..c27b522a 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -15,23 +15,25 @@ use async_trait::async_trait; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::rand::{thread_rng, Rng}; use bitcoin::secp256k1::{Secp256k1, SecretKey}; -use cdk::amount::{Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, +use cdk::amount::{to_unit, Amount}; +use cdk::cdk_payment::{ + self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment, + PaymentQuoteResponse, }; -use cdk::mint::FeeReserve; -use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; -use cdk::util::unix_time; +use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState}; +use cdk::types::FeeReserve; use cdk::{ensure_cdk, mint}; use error::Error; use futures::stream::StreamExt; use futures::Stream; use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tokio::sync::Mutex; use tokio::time; use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; +use tracing::instrument; pub mod error; @@ -49,7 +51,7 @@ pub struct FakeWallet { } impl FakeWallet { - /// Creat new [`FakeWallet`] + /// Create new [`FakeWallet`] pub fn new( fee_reserve: FeeReserve, payment_states: HashMap, @@ -96,40 +98,56 @@ impl Default for FakeInvoiceDescription { } #[async_trait] -impl MintLightning for FakeWallet { - type Err = cdk_lightning::Error; +impl MintPayment for FakeWallet { + type Err = cdk_payment::Error; - fn get_settings(&self) -> Settings { - Settings { + #[instrument(skip_all)] + async fn get_settings(&self) -> Result { + Ok(serde_json::to_value(Bolt11Settings { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, - } + })?) } + #[instrument(skip_all)] fn is_wait_invoice_active(&self) -> bool { self.wait_invoice_is_active.load(Ordering::SeqCst) } + #[instrument(skip_all)] fn cancel_wait_invoice(&self) { self.wait_invoice_cancel_token.cancel() } - async fn wait_any_invoice( + #[instrument(skip_all)] + async fn wait_any_incoming_payment( &self, ) -> Result + Send>>, Self::Err> { + tracing::info!("Starting stream for fake invoices"); let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; let receiver_stream = ReceiverStream::new(receiver); Ok(Box::pin(receiver_stream.map(|label| label))) } + #[instrument(skip_all)] async fn get_payment_quote( &self, - melt_quote_request: &MeltQuoteBolt11Request, + request: &str, + unit: &CurrencyUnit, + options: Option, ) -> Result { - let amount = melt_quote_request.amount_msat()?; + let bolt11 = Bolt11Invoice::from_str(request)?; - let amount = amount / MSAT_IN_SAT.into(); + let amount_msat = match options { + Some(amount) => amount.amount_msat(), + None => bolt11 + .amount_milli_satoshis() + .ok_or(Error::UnknownInvoiceAmount)? + .into(), + }; + + let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?; let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; @@ -142,19 +160,20 @@ impl MintLightning for FakeWallet { }; Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), + request_lookup_id: bolt11.payment_hash().to_string(), amount, fee: fee.into(), state: MeltQuoteState::Unpaid, }) } - async fn pay_invoice( + #[instrument(skip_all)] + async fn make_payment( &self, melt_quote: mint::MeltQuote, _partial_msats: Option, _max_fee_msats: Option, - ) -> Result { + ) -> Result { let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; let payment_hash = bolt11.payment_hash().to_string(); @@ -185,8 +204,8 @@ impl MintLightning for FakeWallet { ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into()); } - Ok(PayInvoiceResponse { - payment_preimage: Some("".to_string()), + Ok(MakePaymentResponse { + payment_proof: Some("".to_string()), payment_lookup_id: payment_hash, status: payment_status, total_spent: melt_quote.amount, @@ -194,16 +213,14 @@ impl MintLightning for FakeWallet { }) } - async fn create_invoice( + #[instrument(skip_all)] + async fn create_incoming_payment_request( &self, amount: Amount, _unit: &CurrencyUnit, description: String, - unix_expiry: u64, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - + _unix_expiry: Option, + ) -> Result { // Since this is fake we just use the amount no matter the unit to create an invoice let amount_msat = amount; @@ -229,24 +246,26 @@ impl MintLightning for FakeWallet { let expiry = invoice.expires_at().map(|t| t.as_secs()); - Ok(CreateInvoiceResponse { + Ok(CreateIncomingPaymentResponse { request_lookup_id: payment_hash.to_string(), - request: invoice, + request: invoice.to_string(), expiry, }) } - async fn check_incoming_invoice_status( + #[instrument(skip_all)] + async fn check_incoming_payment_status( &self, _request_lookup_id: &str, ) -> Result { Ok(MintQuoteState::Paid) } + #[instrument(skip_all)] async fn check_outgoing_payment( &self, request_lookup_id: &str, - ) -> Result { + ) -> Result { // For fake wallet if the state is not explicitly set default to paid let states = self.payment_states.lock().await; let status = states.get(request_lookup_id).cloned(); @@ -256,20 +275,21 @@ impl MintLightning for FakeWallet { let fail_payments = self.failed_payment_check.lock().await; if fail_payments.contains(request_lookup_id) { - return Err(cdk_lightning::Error::InvoicePaymentPending); + return Err(cdk_payment::Error::InvoicePaymentPending); } - Ok(PayInvoiceResponse { - payment_preimage: Some("".to_string()), + Ok(MakePaymentResponse { + payment_proof: Some("".to_string()), payment_lookup_id: request_lookup_id.to_string(), status, total_spent: Amount::ZERO, - unit: self.get_settings().unit, + unit: CurrencyUnit::Msat, }) } } /// Create fake invoice +#[instrument] pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice { let private_key = SecretKey::from_slice( &[ diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 8611665f..89447a06 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use bip39::Mnemonic; use cdk::amount::SplitTarget; use cdk::cdk_database::MintDatabase; -use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; +use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, @@ -15,7 +15,7 @@ use cdk::nuts::{ MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, }; -use cdk::types::QuoteTTL; +use cdk::types::{FeeReserve, QuoteTTL}; use cdk::util::unix_time; use cdk::wallet::client::MintConnector; use cdk::wallet::Wallet; @@ -167,20 +167,22 @@ pub async fn create_and_start_test_mint() -> anyhow::Result> { percent_fee_reserve: 1.0, }; - let ln_fake_backend = Arc::new(FakeWallet::new( + let ln_fake_backend = FakeWallet::new( fee_reserve.clone(), HashMap::default(), HashSet::default(), 0, - )); - - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - MintMeltLimits::new(1, 1_000), - ln_fake_backend, ); + mint_builder = mint_builder + .add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + MintMeltLimits::new(1, 1_000), + Arc::new(ln_fake_backend), + ) + .await?; + let mnemonic = Mnemonic::generate(12)?; mint_builder = mint_builder diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index f94f3a31..2c2edd5a 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; -use cdk::mint::FeeReserve; +use cdk::types::FeeReserve; use cdk_cln::Cln as CdkCln; use cdk_lnd::Lnd as CdkLnd; use ln_regtest_rs::bitcoin_client::BitcoinClient; diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 69fa40fb..37982de6 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -8,14 +8,14 @@ use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::MintDatabase; use cdk::dhke::construct_proofs; -use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote}; +use cdk::mint::{MintBuilder, MintMeltLimits, MintQuote}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod, PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, }; use cdk::subscription::{IndexableParams, Params}; -use cdk::types::QuoteTTL; +use cdk::types::{FeeReserve, QuoteTTL}; use cdk::util::unix_time; use cdk::Mint; use cdk_fake_wallet::FakeWallet; @@ -439,12 +439,14 @@ async fn test_correct_keyset() -> Result<()> { let localstore = Arc::new(database); mint_builder = mint_builder.with_localstore(localstore.clone()); - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - MintMeltLimits::new(1, 5_000), - Arc::new(fake_wallet), - ); + mint_builder = mint_builder + .add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + MintMeltLimits::new(1, 5_000), + Arc::new(fake_wallet), + ) + .await?; mint_builder = mint_builder .with_name("regtest mint".to_string()) diff --git a/crates/cdk-integration-tests/tests/payment_processor.rs b/crates/cdk-integration-tests/tests/payment_processor.rs new file mode 100644 index 00000000..5a6b1ab2 --- /dev/null +++ b/crates/cdk-integration-tests/tests/payment_processor.rs @@ -0,0 +1,176 @@ +//! Tests where we expect the payment processor to respond with an error or pass + +use std::env; +use std::sync::Arc; + +use anyhow::{bail, Result}; +use bip39::Mnemonic; +use cdk::amount::{Amount, SplitTarget}; +use cdk::nuts::nut00::ProofsMethods; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::Wallet; +use cdk_fake_wallet::create_fake_invoice; +use cdk_integration_tests::init_regtest::{get_lnd_dir, get_mint_url, LND_RPC_ADDR}; +use cdk_integration_tests::wait_for_mint_to_be_paid; +use cdk_sqlite::wallet::memory; +use ln_regtest_rs::ln_client::{LightningClient, LndClient}; + +// This is the ln wallet we use to send/receive ln payements as the wallet +async fn init_lnd_client() -> LndClient { + let lnd_dir = get_lnd_dir("one"); + let cert_file = lnd_dir.join("tls.cert"); + let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); + LndClient::new( + format!("https://{}", LND_RPC_ADDR), + cert_file, + macaroon_file, + ) + .await + .unwrap() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_regtest_mint() -> Result<()> { + let wallet = Wallet::new( + &get_mint_url("0"), + CurrencyUnit::Sat, + Arc::new(memory::empty().await?), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_amount = Amount::from(100); + + let mint_quote = wallet.mint_quote(mint_amount, None).await?; + + assert_eq!(mint_quote.amount, mint_amount); + + let ln_backend = env::var("LN_BACKEND")?; + + if ln_backend != "FAKEWALLET" { + let lnd_client = init_lnd_client().await; + + lnd_client.pay_invoice(mint_quote.request).await?; + } + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let proofs = wallet + .mint(&mint_quote.id, SplitTarget::default(), None) + .await?; + + let mint_amount = proofs.total_amount()?; + + assert!(mint_amount == 100.into()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_regtest_mint_melt() -> Result<()> { + let wallet = Wallet::new( + &get_mint_url("0"), + CurrencyUnit::Sat, + Arc::new(memory::empty().await?), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_amount = Amount::from(100); + + let mint_quote = wallet.mint_quote(mint_amount, None).await?; + + assert_eq!(mint_quote.amount, mint_amount); + + let ln_backend = env::var("LN_BACKEND")?; + if ln_backend != "FAKEWALLET" { + let lnd_client = init_lnd_client().await; + + lnd_client.pay_invoice(mint_quote.request).await?; + } + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let proofs = wallet + .mint(&mint_quote.id, SplitTarget::default(), None) + .await?; + + let mint_amount = proofs.total_amount()?; + + assert!(mint_amount == 100.into()); + + let invoice = if ln_backend != "FAKEWALLET" { + let lnd_client = init_lnd_client().await; + lnd_client.create_invoice(Some(50)).await? + } else { + create_fake_invoice(50000, "".to_string()).to_string() + }; + + let melt_quote = wallet.melt_quote(invoice, None).await?; + + wallet.melt(&melt_quote.id).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_pay_invoice_twice() -> Result<()> { + let ln_backend = env::var("LN_BACKEND")?; + if ln_backend == "FAKEWALLET" { + // We can only preform this test on regtest backends as fake wallet just marks the quote as paid + return Ok(()); + } + + let seed = Mnemonic::generate(12)?.to_seed_normalized(""); + let wallet = Wallet::new( + &get_mint_url("0"), + CurrencyUnit::Sat, + Arc::new(memory::empty().await?), + &seed, + None, + )?; + + let mint_quote = wallet.mint_quote(100.into(), None).await?; + + let lnd_client = init_lnd_client().await; + + lnd_client.pay_invoice(mint_quote.request).await?; + + wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?; + + let proofs = wallet + .mint(&mint_quote.id, SplitTarget::default(), None) + .await?; + + let mint_amount = proofs.total_amount()?; + + assert_eq!(mint_amount, 100.into()); + + let invoice = lnd_client.create_invoice(Some(25)).await?; + + let melt_quote = wallet.melt_quote(invoice.clone(), None).await?; + + let melt = wallet.melt(&melt_quote.id).await.unwrap(); + + let melt_two = wallet.melt_quote(invoice, None).await?; + + let melt_two = wallet.melt(&melt_two.id).await; + + match melt_two { + Err(err) => match err { + cdk::Error::RequestAlreadyPaid => (), + err => { + bail!("Wrong invoice already paid: {}", err.to_string()); + } + }, + Ok(_) => { + bail!("Should not have allowed second payment"); + } + } + + let balance = wallet.total_balance().await?; + + assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount)); + + Ok(()) +} diff --git a/crates/cdk-lnbits/Cargo.toml b/crates/cdk-lnbits/Cargo.toml index 54f401d0..c5ae0550 100644 --- a/crates/cdk-lnbits/Cargo.toml +++ b/crates/cdk-lnbits/Cargo.toml @@ -21,3 +21,4 @@ tokio-util.workspace = true tracing.workspace = true thiserror.workspace = true lnbits-rs = "0.4.0" +serde_json.workspace = true diff --git a/crates/cdk-lnbits/src/error.rs b/crates/cdk-lnbits/src/error.rs index dd906f96..7386b827 100644 --- a/crates/cdk-lnbits/src/error.rs +++ b/crates/cdk-lnbits/src/error.rs @@ -19,7 +19,7 @@ pub enum Error { Anyhow(#[from] anyhow::Error), } -impl From for cdk::cdk_lightning::Error { +impl From for cdk::cdk_payment::Error { fn from(e: Error) -> Self { Self::Lightning(Box::new(e)) } diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index fd0c9f6c..c6e72f34 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -4,6 +4,7 @@ #![warn(rustdoc::bare_urls)] use std::pin::Pin; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -11,11 +12,12 @@ use anyhow::anyhow; use async_trait::async_trait; use axum::Router; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, +use cdk::cdk_payment::{ + self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment, + PaymentQuoteResponse, }; -use cdk::mint::FeeReserve; -use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; +use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState}; +use cdk::types::FeeReserve; use cdk::util::unix_time; use cdk::{mint, Bolt11Invoice}; use error::Error; @@ -23,6 +25,7 @@ use futures::stream::StreamExt; use futures::Stream; use lnbits_rs::api::invoice::CreateInvoiceRequest; use lnbits_rs::LNBitsClient; +use serde_json::Value; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; @@ -37,6 +40,7 @@ pub struct LNbits { webhook_url: String, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, + settings: Bolt11Settings, } impl LNbits { @@ -59,20 +63,21 @@ impl LNbits { webhook_url, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), + settings: Bolt11Settings { + mpp: false, + unit: CurrencyUnit::Sat, + invoice_description: true, + }, }) } } #[async_trait] -impl MintLightning for LNbits { - type Err = cdk_lightning::Error; +impl MintPayment for LNbits { + type Err = cdk_payment::Error; - fn get_settings(&self) -> Settings { - Settings { - mpp: false, - unit: CurrencyUnit::Sat, - invoice_description: true, - } + async fn get_settings(&self) -> Result { + Ok(serde_json::to_value(&self.settings)?) } fn is_wait_invoice_active(&self) -> bool { @@ -83,7 +88,7 @@ impl MintLightning for LNbits { self.wait_invoice_cancel_token.cancel() } - async fn wait_any_invoice( + async fn wait_any_incoming_payment( &self, ) -> Result + Send>>, Self::Err> { let receiver = self @@ -145,15 +150,30 @@ impl MintLightning for LNbits { async fn get_payment_quote( &self, - melt_quote_request: &MeltQuoteBolt11Request, + request: &str, + unit: &CurrencyUnit, + options: Option, ) -> Result { - if melt_quote_request.unit != CurrencyUnit::Sat { + if unit != &CurrencyUnit::Sat { return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); } - let amount = melt_quote_request.amount_msat()?; + let bolt11 = Bolt11Invoice::from_str(request)?; - let amount = amount / MSAT_IN_SAT.into(); + let amount_msat = match options { + Some(amount) => { + if matches!(amount, MeltOptions::Mpp { mpp: _ }) { + return Err(cdk_payment::Error::UnsupportedPaymentOption); + } + amount.amount_msat() + } + None => bolt11 + .amount_milli_satoshis() + .ok_or(Error::UnknownInvoiceAmount)? + .into(), + }; + + let amount = amount_msat / MSAT_IN_SAT.into(); let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; @@ -166,19 +186,19 @@ impl MintLightning for LNbits { }; Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), + request_lookup_id: bolt11.payment_hash().to_string(), amount, fee: fee.into(), state: MeltQuoteState::Unpaid, }) } - async fn pay_invoice( + async fn make_payment( &self, melt_quote: mint::MeltQuote, _partial_msats: Option, _max_fee_msats: Option, - ) -> Result { + ) -> Result { let pay_response = self .lnbits_api .pay_invoice(&melt_quote.request, None) @@ -212,36 +232,35 @@ impl MintLightning for LNbits { .unsigned_abs(), ); - Ok(PayInvoiceResponse { + Ok(MakePaymentResponse { payment_lookup_id: pay_response.payment_hash, - payment_preimage: Some(invoice_info.payment_hash), + payment_proof: Some(invoice_info.payment_hash), status, total_spent, unit: CurrencyUnit::Sat, }) } - async fn create_invoice( + async fn create_incoming_payment_request( &self, amount: Amount, unit: &CurrencyUnit, description: String, - unix_expiry: u64, - ) -> Result { + unix_expiry: Option, + ) -> Result { if unit != &CurrencyUnit::Sat { return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); } let time_now = unix_time(); - assert!(unix_expiry > time_now); - let expiry = unix_expiry - time_now; + let expiry = unix_expiry.map(|t| t - time_now); let invoice_request = CreateInvoiceRequest { amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(), memo: Some(description), unit: unit.to_string(), - expiry: Some(expiry), + expiry, webhook: Some(self.webhook_url.clone()), internal: None, out: false, @@ -260,14 +279,14 @@ impl MintLightning for LNbits { let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?; let expiry = request.expires_at().map(|t| t.as_secs()); - Ok(CreateInvoiceResponse { + Ok(CreateIncomingPaymentResponse { request_lookup_id: create_invoice_response.payment_hash, - request, + request: request.to_string(), expiry, }) } - async fn check_incoming_invoice_status( + async fn check_incoming_payment_status( &self, payment_hash: &str, ) -> Result { @@ -292,7 +311,7 @@ impl MintLightning for LNbits { async fn check_outgoing_payment( &self, payment_hash: &str, - ) -> Result { + ) -> Result { let payment = self .lnbits_api .get_payment_info(payment_hash) @@ -303,15 +322,15 @@ impl MintLightning for LNbits { Self::Err::Anyhow(anyhow!("Could not check invoice status")) })?; - let pay_response = PayInvoiceResponse { + let pay_response = MakePaymentResponse { payment_lookup_id: payment.details.payment_hash, - payment_preimage: Some(payment.preimage), + payment_proof: Some(payment.preimage), status: lnbits_to_melt_status(&payment.details.status, payment.details.pending), total_spent: Amount::from( payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs() / MSAT_IN_SAT, ), - unit: self.get_settings().unit, + unit: self.settings.unit.clone(), }; Ok(pay_response) diff --git a/crates/cdk-lnd/Cargo.toml b/crates/cdk-lnd/Cargo.toml index 13e39238..60a7b9a6 100644 --- a/crates/cdk-lnd/Cargo.toml +++ b/crates/cdk-lnd/Cargo.toml @@ -18,3 +18,4 @@ tokio.workspace = true tokio-util.workspace = true tracing.workspace = true thiserror.workspace = true +serde_json.workspace = true diff --git a/crates/cdk-lnd/src/error.rs b/crates/cdk-lnd/src/error.rs index ffff7e61..b3a28229 100644 --- a/crates/cdk-lnd/src/error.rs +++ b/crates/cdk-lnd/src/error.rs @@ -38,7 +38,7 @@ pub enum Error { InvalidConfig(String), } -impl From for cdk::cdk_lightning::Error { +impl From for cdk::cdk_payment::Error { fn from(e: Error) -> Self { Self::Lightning(Box::new(e)) } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index f107817c..78b8bf78 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -14,13 +14,14 @@ use std::sync::Arc; use anyhow::anyhow; use async_trait::async_trait; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; -use cdk::cdk_lightning::{ - self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, +use cdk::cdk_payment::{ + self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment, + PaymentQuoteResponse, }; -use cdk::mint::FeeReserve; -use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; +use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState}; use cdk::secp256k1::hashes::Hash; -use cdk::util::{hex, unix_time}; +use cdk::types::FeeReserve; +use cdk::util::hex; use cdk::{mint, Bolt11Invoice}; use error::Error; use fedimint_tonic_lnd::lnrpc::fee_limit::Limit; @@ -45,6 +46,7 @@ pub struct Lnd { fee_reserve: FeeReserve, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, + settings: Bolt11Settings, } impl Lnd { @@ -96,21 +98,22 @@ impl Lnd { fee_reserve, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), + settings: Bolt11Settings { + mpp: true, + unit: CurrencyUnit::Msat, + invoice_description: true, + }, }) } } #[async_trait] -impl MintLightning for Lnd { - type Err = cdk_lightning::Error; +impl MintPayment for Lnd { + type Err = cdk_payment::Error; #[instrument(skip_all)] - fn get_settings(&self) -> Settings { - Settings { - mpp: true, - unit: CurrencyUnit::Msat, - invoice_description: true, - } + async fn get_settings(&self) -> Result { + Ok(serde_json::to_value(&self.settings)?) } #[instrument(skip_all)] @@ -124,7 +127,7 @@ impl MintLightning for Lnd { } #[instrument(skip_all)] - async fn wait_any_invoice( + async fn wait_any_incoming_payment( &self, ) -> Result + Send>>, Self::Err> { let mut client = @@ -183,7 +186,7 @@ impl MintLightning for Lnd { }, // End of stream Err(err) => { is_active.store(false, Ordering::SeqCst); - tracing::warn!("Encounrdered error in LND invoice stream. Stream ending"); + tracing::warn!("Encountered error in LND invoice stream. Stream ending"); tracing::error!("{:?}", err); None @@ -199,11 +202,21 @@ impl MintLightning for Lnd { #[instrument(skip_all)] async fn get_payment_quote( &self, - melt_quote_request: &MeltQuoteBolt11Request, + request: &str, + unit: &CurrencyUnit, + options: Option, ) -> Result { - let amount = melt_quote_request.amount_msat()?; + let bolt11 = Bolt11Invoice::from_str(request)?; - let amount = amount / MSAT_IN_SAT.into(); + let amount_msat = match options { + Some(amount) => amount.amount_msat(), + None => bolt11 + .amount_milli_satoshis() + .ok_or(Error::UnknownInvoiceAmount)? + .into(), + }; + + let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?; let relative_fee_reserve = (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; @@ -216,7 +229,7 @@ impl MintLightning for Lnd { }; Ok(PaymentQuoteResponse { - request_lookup_id: melt_quote_request.request.payment_hash().to_string(), + request_lookup_id: bolt11.payment_hash().to_string(), amount, fee: fee.into(), state: MeltQuoteState::Unpaid, @@ -224,12 +237,12 @@ impl MintLightning for Lnd { } #[instrument(skip_all)] - async fn pay_invoice( + async fn make_payment( &self, melt_quote: mint::MeltQuote, partial_amount: Option, max_fee: Option, - ) -> Result { + ) -> Result { let payment_request = melt_quote.request; let bolt11 = Bolt11Invoice::from_str(&payment_request)?; @@ -347,9 +360,9 @@ impl MintLightning for Lnd { total_amt = (route.total_amt_msat / 1000) as u64; } - Ok(PayInvoiceResponse { + Ok(MakePaymentResponse { payment_lookup_id: hex::encode(payment_hash), - payment_preimage, + payment_proof: payment_preimage, status, total_spent: total_amt.into(), unit: CurrencyUnit::Sat, @@ -393,9 +406,9 @@ impl MintLightning for Lnd { ), }; - Ok(PayInvoiceResponse { + Ok(MakePaymentResponse { payment_lookup_id: hex::encode(payment_response.payment_hash), - payment_preimage, + payment_proof: payment_preimage, status, total_spent: total_amount.into(), unit: CurrencyUnit::Sat, @@ -405,16 +418,13 @@ impl MintLightning for Lnd { } #[instrument(skip(self, description))] - async fn create_invoice( + async fn create_incoming_payment_request( &self, amount: Amount, unit: &CurrencyUnit, description: String, - unix_expiry: u64, - ) -> Result { - let time_now = unix_time(); - assert!(unix_expiry > time_now); - + unix_expiry: Option, + ) -> Result { let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice { @@ -435,15 +445,15 @@ impl MintLightning for Lnd { let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?; - Ok(CreateInvoiceResponse { + Ok(CreateIncomingPaymentResponse { request_lookup_id: bolt11.payment_hash().to_string(), - request: bolt11, - expiry: Some(unix_expiry), + request: bolt11.to_string(), + expiry: unix_expiry, }) } #[instrument(skip(self))] - async fn check_incoming_invoice_status( + async fn check_incoming_payment_status( &self, request_lookup_id: &str, ) -> Result { @@ -479,7 +489,7 @@ impl MintLightning for Lnd { async fn check_outgoing_payment( &self, payment_hash: &str, - ) -> Result { + ) -> Result { let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest { payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?, no_inflight_updates: true, @@ -498,15 +508,15 @@ impl MintLightning for Lnd { Err(err) => { let err_code = err.code(); if err_code == Code::NotFound { - return Ok(PayInvoiceResponse { + return Ok(MakePaymentResponse { payment_lookup_id: payment_hash.to_string(), - payment_preimage: None, + payment_proof: None, status: MeltQuoteState::Unknown, total_spent: Amount::ZERO, - unit: self.get_settings().unit, + unit: self.settings.unit.clone(), }); } else { - return Err(cdk_lightning::Error::UnknownPaymentState); + return Err(cdk_payment::Error::UnknownPaymentState); } } }; @@ -517,20 +527,20 @@ impl MintLightning for Lnd { let status = update.status(); let response = match status { - PaymentStatus::Unknown => PayInvoiceResponse { + PaymentStatus::Unknown => MakePaymentResponse { payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), + payment_proof: Some(update.payment_preimage), status: MeltQuoteState::Unknown, total_spent: Amount::ZERO, - unit: self.get_settings().unit, + unit: self.settings.unit.clone(), }, PaymentStatus::InFlight => { // Continue waiting for the next update continue; } - PaymentStatus::Succeeded => PayInvoiceResponse { + PaymentStatus::Succeeded => MakePaymentResponse { payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), + payment_proof: Some(update.payment_preimage), status: MeltQuoteState::Paid, total_spent: Amount::from( (update @@ -541,12 +551,12 @@ impl MintLightning for Lnd { ), unit: CurrencyUnit::Sat, }, - PaymentStatus::Failed => PayInvoiceResponse { + PaymentStatus::Failed => MakePaymentResponse { payment_lookup_id: payment_hash.to_string(), - payment_preimage: Some(update.payment_preimage), + payment_proof: Some(update.payment_preimage), status: MeltQuoteState::Failed, total_spent: Amount::ZERO, - unit: self.get_settings().unit, + unit: self.settings.unit.clone(), }, }; diff --git a/crates/cdk-mint-rpc/Cargo.toml b/crates/cdk-mint-rpc/Cargo.toml index d191438e..6aeb1b68 100644 --- a/crates/cdk-mint-rpc/Cargo.toml +++ b/crates/cdk-mint-rpc/Cargo.toml @@ -19,20 +19,16 @@ cdk = { workspace = true, features = [ "mint", ] } clap.workspace = true -tonic = { version = "0.12.3", features = [ - "channel", - "tls", - "tls-webpki-roots", -] } +tonic.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tokio.workspace = true serde_json.workspace = true serde.workspace = true thiserror.workspace = true -prost = "0.13.1" +prost.workspace = true home.workspace = true [build-dependencies] -tonic-build = "0.12" +tonic-build.workspace = true diff --git a/crates/cdk-mintd/Cargo.toml b/crates/cdk-mintd/Cargo.toml index 55393972..5c3d5871 100644 --- a/crates/cdk-mintd/Cargo.toml +++ b/crates/cdk-mintd/Cargo.toml @@ -10,18 +10,19 @@ description = "CDK mint binary" rust-version = "1.75.0" [features] -default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet"] +default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor"] # Ensure at least one lightning backend is enabled -swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] -redis = ["cdk-axum/redis"] management-rpc = ["cdk-mint-rpc"] -# MSRV is not commited to with redb enabled -redb = ["dep:cdk-redb"] -sqlcipher = ["cdk-sqlite/sqlcipher"] cln = ["dep:cdk-cln"] lnd = ["dep:cdk-lnd"] lnbits = ["dep:cdk-lnbits"] fakewallet = ["dep:cdk-fake-wallet"] +grpc-processor = ["dep:cdk-payment-processor"] +sqlcipher = ["cdk-sqlite/sqlcipher"] +# MSRV is not commited to with redb enabled +redb = ["dep:cdk-redb"] +swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] +redis = ["cdk-axum/redis"] [dependencies] anyhow.workspace = true @@ -42,6 +43,7 @@ cdk-lnd = { workspace = true, optional = true } cdk-fake-wallet = { workspace = true, optional = true } cdk-axum.workspace = true cdk-mint-rpc = { workspace = true, optional = true } +cdk-payment-processor = { workspace = true, optional = true } config = { version = "0.13.3", features = ["toml"] } clap.workspace = true bitcoin.workspace = true @@ -54,7 +56,7 @@ bip39.workspace = true tower-http = { workspace = true, features = ["compression-full", "decompression-full"] } tower = "0.5.2" lightning-invoice.workspace = true -home = "0.5.5" +home.workspace = true url.workspace = true utoipa = { workspace = true, optional = true } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true } diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index 40342f3e..8adb6d61 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -91,3 +91,10 @@ reserve_fee_min = 4 # reserve_fee_min = 1 # min_delay_time = 1 # max_delay_time = 3 + +# [grpc_processor] +# gRPC Payment Processor configuration +# supported_units = ["sat"] +# addr = "127.0.0.1" +# port = 50051 +# tls_dir = "/path/to/tls" diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 5c6c10d0..9b8a4729 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -1,9 +1,7 @@ use std::path::PathBuf; use bitcoin::hashes::{sha256, Hash}; -#[cfg(feature = "fakewallet")] -use cdk::nuts::CurrencyUnit; -use cdk::nuts::PublicKey; +use cdk::nuts::{CurrencyUnit, PublicKey}; use cdk::Amount; use cdk_axum::cache; use config::{Config, ConfigError, File}; @@ -56,6 +54,8 @@ pub enum LnBackend { FakeWallet, #[cfg(feature = "lnd")] Lnd, + #[cfg(feature = "grpc-processor")] + GrpcProcessor, } impl std::str::FromStr for LnBackend { @@ -71,6 +71,8 @@ impl std::str::FromStr for LnBackend { "fakewallet" => Ok(LnBackend::FakeWallet), #[cfg(feature = "lnd")] "lnd" => Ok(LnBackend::Lnd), + #[cfg(feature = "grpc-processor")] + "grpcprocessor" => Ok(LnBackend::GrpcProcessor), _ => Err(format!("Unknown Lightning backend: {}", s)), } } @@ -165,6 +167,14 @@ fn default_max_delay_time() -> u64 { 3 } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub struct GrpcProcessor { + pub supported_units: Vec, + pub addr: String, + pub port: u16, + pub tls_dir: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "lowercase")] pub enum DatabaseEngine { @@ -206,6 +216,7 @@ pub struct Settings { pub lnd: Option, #[cfg(feature = "fakewallet")] pub fake_wallet: Option, + pub grpc_processor: Option, pub database: Database, #[cfg(feature = "management-rpc")] pub mint_management_rpc: Option, @@ -313,6 +324,13 @@ impl Settings { settings.fake_wallet.is_some(), "FakeWallet backend requires a valid config." ), + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + assert!( + settings.grpc_processor.is_some(), + "GRPC backend requires a valid config." + ) + } } Ok(settings) diff --git a/crates/cdk-mintd/src/env_vars/grpc_processor.rs b/crates/cdk-mintd/src/env_vars/grpc_processor.rs new file mode 100644 index 00000000..e7aa5cfb --- /dev/null +++ b/crates/cdk-mintd/src/env_vars/grpc_processor.rs @@ -0,0 +1,44 @@ +//! gRPC Payment Processor environment variables + +use std::env; + +use cdk::nuts::CurrencyUnit; + +use crate::config::GrpcProcessor; + +// gRPC Payment Processor environment variables +pub const ENV_GRPC_PROCESSOR_SUPPORTED_UNITS: &str = + "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS"; +pub const ENV_GRPC_PROCESSOR_ADDRESS: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS"; +pub const ENV_GRPC_PROCESSOR_PORT: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT"; +pub const ENV_GRPC_PROCESSOR_TLS_DIR: &str = "CDK_MINTD_GRPC_PAYMENT_PROCESSOR_TLS_DIR"; + +impl GrpcProcessor { + pub fn from_env(mut self) -> Self { + if let Ok(units_str) = env::var(ENV_GRPC_PROCESSOR_SUPPORTED_UNITS) { + if let Ok(units) = units_str + .split(',') + .map(|s| s.trim().parse()) + .collect::, _>>() + { + self.supported_units = units; + } + } + + if let Ok(addr) = env::var(ENV_GRPC_PROCESSOR_ADDRESS) { + self.addr = addr; + } + + if let Ok(port) = env::var(ENV_GRPC_PROCESSOR_PORT) { + if let Ok(port) = port.parse() { + self.port = port; + } + } + + if let Ok(tls_dir) = env::var(ENV_GRPC_PROCESSOR_TLS_DIR) { + self.tls_dir = Some(tls_dir.into()); + } + + self + } +} diff --git a/crates/cdk-mintd/src/env_vars/ln.rs b/crates/cdk-mintd/src/env_vars/ln.rs index ba76702d..96f6e33a 100644 --- a/crates/cdk-mintd/src/env_vars/ln.rs +++ b/crates/cdk-mintd/src/env_vars/ln.rs @@ -18,6 +18,8 @@ impl Ln { if let Ok(backend_str) = env::var(ENV_LN_BACKEND) { if let Ok(backend) = backend_str.parse() { self.ln_backend = backend; + } else { + tracing::warn!("Unknow payment backend set in env var will attempt to use config file. {backend_str}"); } } diff --git a/crates/cdk-mintd/src/env_vars/mod.rs b/crates/cdk-mintd/src/env_vars/mod.rs index 492595ac..1be11cee 100644 --- a/crates/cdk-mintd/src/env_vars/mod.rs +++ b/crates/cdk-mintd/src/env_vars/mod.rs @@ -12,6 +12,8 @@ mod mint_info; mod cln; #[cfg(feature = "fakewallet")] mod fake_wallet; +#[cfg(feature = "grpc-processor")] +mod grpc_processor; #[cfg(feature = "lnbits")] mod lnbits; #[cfg(feature = "lnd")] @@ -28,6 +30,8 @@ pub use cln::*; pub use common::*; #[cfg(feature = "fakewallet")] pub use fake_wallet::*; +#[cfg(feature = "grpc-processor")] +pub use grpc_processor::*; pub use ln::*; #[cfg(feature = "lnbits")] pub use lnbits::*; @@ -77,6 +81,11 @@ impl Settings { LnBackend::Lnd => { self.lnd = Some(self.lnd.clone().unwrap_or_default().from_env()); } + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + self.grpc_processor = + Some(self.grpc_processor.clone().unwrap_or_default().from_env()); + } LnBackend::None => bail!("Ln backend must be set"), #[allow(unreachable_patterns)] _ => bail!("Selected Ln backend is not enabled in this build"), diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 1dc18e0d..6863d10d 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -19,11 +19,19 @@ use cdk::mint::{MintBuilder, MintMeltLimits}; feature = "cln", feature = "lnbits", feature = "lnd", - feature = "fakewallet" + feature = "fakewallet", + feature = "grpc-processor" ))] use cdk::nuts::nut17::SupportedMethods; use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; -use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; +#[cfg(any( + feature = "cln", + feature = "lnbits", + feature = "lnd", + feature = "fakewallet" +))] +use cdk::nuts::CurrencyUnit; +use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod}; use cdk::types::QuoteTTL; use cdk_axum::cache::HttpCache; #[cfg(feature = "management-rpc")] @@ -52,10 +60,11 @@ const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION") feature = "cln", feature = "lnbits", feature = "lnd", - feature = "fakewallet" + feature = "fakewallet", + feature = "grpc-processor" )))] compile_error!( - "At least one lightning backend feature must be enabled: cln, lnbits, lnd, or fakewallet" + "At least one lightning backend feature must be enabled: cln, lnbits, lnd, fakewallet, or grpc-processor" ); #[tokio::main] @@ -169,6 +178,8 @@ async fn main() -> anyhow::Result<()> { melt_max: settings.ln.max_melt, }; + tracing::debug!("Ln backendd: {:?}", settings.ln.ln_backend); + match settings.ln.ln_backend { #[cfg(feature = "cln")] LnBackend::Cln => { @@ -182,12 +193,14 @@ async fn main() -> anyhow::Result<()> { .await?; let cln = Arc::new(cln); - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - cln.clone(), - ); + mint_builder = mint_builder + .add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + cln.clone(), + ) + .await?; let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); @@ -200,12 +213,15 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .await?; - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(lnbits), - ); + mint_builder = mint_builder + .add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(lnbits), + ) + .await?; + let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); mint_builder = mint_builder.add_supported_websockets(nut17_supported); @@ -217,12 +233,14 @@ async fn main() -> anyhow::Result<()> { .setup(&mut ln_routers, &settings, CurrencyUnit::Msat) .await?; - mint_builder = mint_builder.add_ln_backend( - CurrencyUnit::Sat, - PaymentMethod::Bolt11, - mint_melt_limits, - Arc::new(lnd), - ); + mint_builder = mint_builder + .add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(lnd), + ) + .await?; let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); @@ -231,27 +249,72 @@ async fn main() -> anyhow::Result<()> { #[cfg(feature = "fakewallet")] LnBackend::FakeWallet => { let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); + tracing::info!("Using fake wallet: {:?}", fake_wallet); for unit in fake_wallet.clone().supported_units { let fake = fake_wallet .setup(&mut ln_routers, &settings, CurrencyUnit::Sat) - .await?; + .await + .expect("hhh"); let fake = Arc::new(fake); - mint_builder = mint_builder.add_ln_backend( - unit.clone(), - PaymentMethod::Bolt11, - mint_melt_limits, - fake.clone(), - ); + mint_builder = mint_builder + .add_ln_backend( + unit.clone(), + PaymentMethod::Bolt11, + mint_melt_limits, + fake.clone(), + ) + .await?; let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit); mint_builder = mint_builder.add_supported_websockets(nut17_supported); } } - LnBackend::None => bail!("Ln backend must be set"), + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + let grpc_processor = settings + .clone() + .grpc_processor + .expect("grpc processor config defined"); + + tracing::info!( + "Attempting to start with gRPC payment processor at {}:{}.", + grpc_processor.addr, + grpc_processor.port + ); + + tracing::info!("{:?}", grpc_processor); + + for unit in grpc_processor.clone().supported_units { + tracing::debug!("Adding unit: {:?}", unit); + + let processor = grpc_processor + .setup(&mut ln_routers, &settings, unit.clone()) + .await?; + + mint_builder = mint_builder + .add_ln_backend( + unit.clone(), + PaymentMethod::Bolt11, + mint_melt_limits, + Arc::new(processor), + ) + .await?; + + let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit); + mint_builder = mint_builder.add_supported_websockets(nut17_supported); + } + } + LnBackend::None => { + tracing::error!( + "Pyament backend was not set or feature disabled. {:?}", + settings.ln.ln_backend + ); + bail!("Ln backend must be") + } }; if let Some(long_description) = &settings.mint_info.description_long { @@ -300,6 +363,8 @@ async fn main() -> anyhow::Result<()> { let mint = mint_builder.build().await?; + tracing::debug!("Mint built from builder."); + let mint = Arc::new(mint); // Check the status of any mint quotes that are pending @@ -425,6 +490,13 @@ async fn main() -> anyhow::Result<()> { Ok(()) } +async fn shutdown_signal() { + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler"); + tracing::info!("Shutdown signal received"); +} + fn work_dir() -> Result { let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; let dir = home_dir.join(".cdk-mintd"); @@ -433,10 +505,3 @@ fn work_dir() -> Result { Ok(dir) } - -async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C handler"); - tracing::info!("Shutdown signal received"); -} diff --git a/crates/cdk-mintd/src/setup.rs b/crates/cdk-mintd/src/setup.rs index 11e3b695..1ffbb7e1 100644 --- a/crates/cdk-mintd/src/setup.rs +++ b/crates/cdk-mintd/src/setup.rs @@ -11,11 +11,17 @@ use async_trait::async_trait; use axum::Router; #[cfg(feature = "fakewallet")] use bip39::rand::{thread_rng, Rng}; -use cdk::cdk_lightning::MintLightning; -use cdk::mint::FeeReserve; +use cdk::cdk_payment::MintPayment; #[cfg(feature = "lnbits")] use cdk::mint_url::MintUrl; use cdk::nuts::CurrencyUnit; +#[cfg(any( + feature = "lnbits", + feature = "cln", + feature = "lnd", + feature = "fakewallet" +))] +use cdk::types::FeeReserve; #[cfg(feature = "lnbits")] use tokio::sync::Mutex; @@ -30,7 +36,7 @@ pub trait LnBackendSetup { routers: &mut Vec, settings: &Settings, unit: CurrencyUnit, - ) -> anyhow::Result; + ) -> anyhow::Result; } #[cfg(feature = "cln")] @@ -162,3 +168,23 @@ impl LnBackendSetup for config::FakeWallet { Ok(fake_wallet) } } + +#[cfg(feature = "grpc-processor")] +#[async_trait] +impl LnBackendSetup for config::GrpcProcessor { + async fn setup( + &self, + _routers: &mut Vec, + _settings: &Settings, + _unit: CurrencyUnit, + ) -> anyhow::Result { + let payment_processor = cdk_payment_processor::PaymentProcessorClient::new( + &self.addr, + self.port, + self.tls_dir.clone(), + ) + .await?; + + Ok(payment_processor) + } +} diff --git a/crates/cdk-payment-processor/Cargo.toml b/crates/cdk-payment-processor/Cargo.toml new file mode 100644 index 00000000..e4f546a7 --- /dev/null +++ b/crates/cdk-payment-processor/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "cdk-payment-processor" +version = "0.7.1" +edition = "2021" +authors = ["CDK Developers"] +description = "CDK payment processor" +homepage = "https://github.com/cashubtc/cdk" +repository = "https://github.com/cashubtc/cdk.git" +rust-version = "1.75.0" # MSRV +license = "MIT" + +[[bin]] +name = "cdk-payment-processor" +path = "src/bin/payment_processor.rs" + +[features] +default = ["cln", "fake", "lnd"] +bench = [] +cln = ["dep:cdk-cln"] +fake = ["dep:cdk-fake-wallet"] +lnd = ["dep:cdk-lnd"] + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +bitcoin.workspace = true +cdk-common = { workspace = true, features = ["mint"] } +cdk-cln = { workspace = true, optional = true } +cdk-lnd = { workspace = true, optional = true } +cdk-fake-wallet = { workspace = true, optional = true } +serde.workspace = true +thiserror.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +lightning-invoice.workspace = true +uuid = { workspace = true, optional = true } +utoipa = { workspace = true, optional = true } +futures.workspace = true +serde_json.workspace = true +serde_with.workspace = true +tonic.workspace = true +prost.workspace = true +tokio-stream.workspace = true +tokio-util = { workspace = true, default-features = false } + + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { workspace = true, features = [ + "rt-multi-thread", + "time", + "macros", + "sync", + "signal" +] } + + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] } + +[dev-dependencies] +rand.workspace = true +bip39.workspace = true + +[build-dependencies] +tonic-build.workspace = true diff --git a/crates/cdk-payment-processor/README.md b/crates/cdk-payment-processor/README.md new file mode 100644 index 00000000..d32ebaf2 --- /dev/null +++ b/crates/cdk-payment-processor/README.md @@ -0,0 +1,77 @@ +# CDK Payment Processor + +The cdk-payment-processor is a Rust crate that provides both a binary and a library for handling payments to and from a cdk mint. + +## Overview + +### Library Components +- **Payment Processor Server**: Handles interaction with payment processor backend implementations +- **Client**: Used by mintd to query the server for payment information +- **Backend Implementations**: Supports CLN, LND, and a fake wallet (for testing) + +### Features +- Modular backend system supporting multiple Lightning implementations +- Extensible design allowing for custom backend implementations + +## Building from Source + +### Prerequisites +1. Install Nix package manager +2. Enter development environment: +```sh +nix develop +``` + +### Configuration + +The server requires different environment variables depending on your chosen Lightning Network backend. + +#### Core Settings +```sh +# Choose backend: CLN, LND, or FAKEWALLET +export CDK_PAYMENT_PROCESSOR_LN_BACKEND="CLN" + +# Server configuration +export CDK_PAYMENT_PROCESSOR_LISTEN_HOST="127.0.0.1" +export CDK_PAYMENT_PROCESSOR_LISTEN_PORT="8090" +``` + +#### Backend-Specific Configuration + +##### Core Lightning (CLN) +```sh +# Path to CLN RPC socket +export CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH="/path/to/lightning-rpc" +``` + +##### Lightning Network Daemon (LND) +```sh +# LND connection details +export CDK_PAYMENT_PROCESSOR_LND_ADDRESS="localhost:10009" +export CDK_PAYMENT_PROCESSOR_LND_CERT_FILE="/path/to/tls.cert" +export CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE="/path/to/macaroon" +``` + +### Building and Running + +Build and run the binary with your chosen backend: + +```sh +# For CLN backend +cargo run --bin cdk-payment-processor --no-default-features --features cln + +# For LND backend +cargo run --bin cdk-payment-processor --no-default-features --features lnd + +# For fake wallet (testing only) +cargo run --bin cdk-payment-processor --no-default-features --features fake +``` + +## Development + +To implement a new backend: +1. Create a new module implementing the payment processor traits +2. Add appropriate feature flags +3. Update the binary to support the new backend + +For library usage examples and API documentation, refer to the crate documentation. diff --git a/crates/cdk-payment-processor/build.rs b/crates/cdk-payment-processor/build.rs new file mode 100644 index 00000000..853d3257 --- /dev/null +++ b/crates/cdk-payment-processor/build.rs @@ -0,0 +1,5 @@ +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=src/proto/payment_processor.proto"); + tonic_build::compile_protos("src/proto/payment_processor.proto")?; + Ok(()) +} diff --git a/crates/cdk-payment-processor/src/bin/payment_processor.rs b/crates/cdk-payment-processor/src/bin/payment_processor.rs new file mode 100644 index 00000000..bd45b7f3 --- /dev/null +++ b/crates/cdk-payment-processor/src/bin/payment_processor.rs @@ -0,0 +1,206 @@ +#[cfg(feature = "fake")] +use std::collections::{HashMap, HashSet}; +use std::env; +use std::path::PathBuf; +#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] +use std::sync::Arc; + +#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] +use anyhow::bail; +#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] +use cdk_common::common::FeeReserve; +#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] +use cdk_common::payment::{self, MintPayment}; +use cdk_common::Amount; +#[cfg(feature = "fake")] +use cdk_fake_wallet::FakeWallet; +use serde::{Deserialize, Serialize}; +#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] +use tokio::signal; +use tracing_subscriber::EnvFilter; + +pub const ENV_LN_BACKEND: &str = "CDK_PAYMENT_PROCESSOR_LN_BACKEND"; +pub const ENV_LISTEN_HOST: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_HOST"; +pub const ENV_LISTEN_PORT: &str = "CDK_PAYMENT_PROCESSOR_LISTEN_PORT"; +pub const ENV_PAYMENT_PROCESSOR_TLS_DIR: &str = "CDK_PAYMENT_PROCESSOR_TLS_DIR"; + +// CLN +pub const ENV_CLN_RPC_PATH: &str = "CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH"; +pub const ENV_CLN_BOLT12: &str = "CDK_PAYMENT_PROCESSOR_CLN_BOLT12"; + +pub const ENV_FEE_PERCENT: &str = "CDK_PAYMENT_PROCESSOR_FEE_PERCENT"; +pub const ENV_RESERVE_FEE_MIN: &str = "CDK_PAYMENT_PROCESSOR_RESERVE_FEE_MIN"; + +// LND environment variables +pub const ENV_LND_ADDRESS: &str = "CDK_PAYMENT_PROCESSOR_LND_ADDRESS"; +pub const ENV_LND_CERT_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_CERT_FILE"; +pub const ENV_LND_MACAROON_FILE: &str = "CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let default_filter = "debug"; + + let sqlx_filter = "sqlx=warn"; + let hyper_filter = "hyper=warn"; + let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; + + let env_filter = EnvFilter::new(format!( + "{},{},{},{},{}", + default_filter, sqlx_filter, hyper_filter, h2_filter, rustls_filter + )); + + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + #[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))] + { + let ln_backend: String = env::var(ENV_LN_BACKEND)?; + let listen_addr: String = env::var(ENV_LISTEN_HOST)?; + let listen_port: u16 = env::var(ENV_LISTEN_PORT)?.parse()?; + let tls_dir: Option = env::var(ENV_PAYMENT_PROCESSOR_TLS_DIR) + .ok() + .map(PathBuf::from); + + let ln_backed: Arc + Send + Sync> = + match ln_backend.to_uppercase().as_str() { + #[cfg(feature = "cln")] + "CLN" => { + let cln_settings = Cln::default().from_env(); + let fee_reserve = FeeReserve { + min_fee_reserve: cln_settings.reserve_fee_min, + percent_fee_reserve: cln_settings.fee_percent, + }; + + Arc::new(cdk_cln::Cln::new(cln_settings.rpc_path, fee_reserve).await?) + } + #[cfg(feature = "fake")] + "FAKEWALLET" => { + let fee_reserve = FeeReserve { + min_fee_reserve: 1.into(), + percent_fee_reserve: 0.0, + }; + + let fake_wallet = + FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0); + + Arc::new(fake_wallet) + } + #[cfg(feature = "lnd")] + "LND" => { + let lnd_settings = Lnd::default().from_env(); + let fee_reserve = FeeReserve { + min_fee_reserve: lnd_settings.reserve_fee_min, + percent_fee_reserve: lnd_settings.fee_percent, + }; + + Arc::new( + cdk_lnd::Lnd::new( + lnd_settings.address, + lnd_settings.cert_file, + lnd_settings.macaroon_file, + fee_reserve, + ) + .await?, + ) + } + + _ => { + bail!("Unknown payment processor"); + } + }; + + let mut server = cdk_payment_processor::PaymentProcessorServer::new( + ln_backed, + &listen_addr, + listen_port, + )?; + + server.start(tls_dir).await?; + + // Wait for shutdown signal + signal::ctrl_c().await?; + + server.stop().await?; + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Cln { + pub rpc_path: PathBuf, + #[serde(default)] + pub bolt12: bool, + pub fee_percent: f32, + pub reserve_fee_min: Amount, +} + +impl Cln { + pub fn from_env(mut self) -> Self { + // RPC Path + if let Ok(path) = env::var(ENV_CLN_RPC_PATH) { + self.rpc_path = PathBuf::from(path); + } + + // BOLT12 flag + if let Ok(bolt12_str) = env::var(ENV_CLN_BOLT12) { + if let Ok(bolt12) = bolt12_str.parse() { + self.bolt12 = bolt12; + } + } + + // Fee percent + if let Ok(fee_str) = env::var(ENV_FEE_PERCENT) { + if let Ok(fee) = fee_str.parse() { + self.fee_percent = fee; + } + } + + // Reserve fee minimum + if let Ok(reserve_fee_str) = env::var(ENV_RESERVE_FEE_MIN) { + if let Ok(reserve_fee) = reserve_fee_str.parse::() { + self.reserve_fee_min = reserve_fee.into(); + } + } + + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Lnd { + pub address: String, + pub cert_file: PathBuf, + pub macaroon_file: PathBuf, + pub fee_percent: f32, + pub reserve_fee_min: Amount, +} + +impl Lnd { + pub fn from_env(mut self) -> Self { + if let Ok(address) = env::var(ENV_LND_ADDRESS) { + self.address = address; + } + + if let Ok(cert_path) = env::var(ENV_LND_CERT_FILE) { + self.cert_file = PathBuf::from(cert_path); + } + + if let Ok(macaroon_path) = env::var(ENV_LND_MACAROON_FILE) { + self.macaroon_file = PathBuf::from(macaroon_path); + } + + if let Ok(fee_str) = env::var(ENV_FEE_PERCENT) { + if let Ok(fee) = fee_str.parse() { + self.fee_percent = fee; + } + } + + if let Ok(reserve_fee_str) = env::var(ENV_RESERVE_FEE_MIN) { + if let Ok(reserve_fee) = reserve_fee_str.parse::() { + self.reserve_fee_min = reserve_fee.into(); + } + } + + self + } +} diff --git a/crates/cdk-payment-processor/src/error.rs b/crates/cdk-payment-processor/src/error.rs new file mode 100644 index 00000000..a4c27251 --- /dev/null +++ b/crates/cdk-payment-processor/src/error.rs @@ -0,0 +1,20 @@ +//! Errors + +use thiserror::Error; + +/// CDK Payment processor error +#[derive(Debug, Error)] +pub enum Error { + /// Invalid ID + #[error("Invalid id")] + InvalidId, + /// NUT00 Error + #[error(transparent)] + NUT00(#[from] cdk_common::nuts::nut00::Error), + /// NUT05 error + #[error(transparent)] + NUT05(#[from] cdk_common::nuts::nut05::Error), + /// Parse invoice error + #[error(transparent)] + Invoice(#[from] lightning_invoice::ParseOrSemanticError), +} diff --git a/crates/cdk-payment-processor/src/lib.rs b/crates/cdk-payment-processor/src/lib.rs new file mode 100644 index 00000000..a39b9bd0 --- /dev/null +++ b/crates/cdk-payment-processor/src/lib.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod proto; + +pub use proto::cdk_payment_processor_client::CdkPaymentProcessorClient; +pub use proto::cdk_payment_processor_server::CdkPaymentProcessorServer; +pub use proto::{PaymentProcessorClient, PaymentProcessorServer}; +#[doc(hidden)] +pub use tonic; diff --git a/crates/cdk-payment-processor/src/proto/client.rs b/crates/cdk-payment-processor/src/proto/client.rs new file mode 100644 index 00000000..40355b9b --- /dev/null +++ b/crates/cdk-payment-processor/src/proto/client.rs @@ -0,0 +1,299 @@ +use std::path::PathBuf; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use anyhow::anyhow; +use cdk_common::payment::{ + CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse, MintPayment, + PaymentQuoteResponse, +}; +use cdk_common::{mint, Amount, CurrencyUnit, MeltOptions, MintQuoteState}; +use futures::{Stream, StreamExt}; +use serde_json::Value; +use tokio_util::sync::CancellationToken; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; +use tonic::{async_trait, Request}; +use tracing::instrument; + +use super::cdk_payment_processor_client::CdkPaymentProcessorClient; +use super::{ + CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest, + MakePaymentRequest, SettingsRequest, WaitIncomingPaymentRequest, +}; + +/// Payment Processor +#[derive(Clone)] +pub struct PaymentProcessorClient { + inner: CdkPaymentProcessorClient, + wait_incoming_payment_stream_is_active: Arc, + cancel_incoming_payment_listener: CancellationToken, +} + +impl PaymentProcessorClient { + /// Payment Processor + pub async fn new(addr: &str, port: u16, tls_dir: Option) -> anyhow::Result { + let addr = format!("{}:{}", addr, port); + let channel = if let Some(tls_dir) = tls_dir { + // TLS directory exists, configure TLS + + // Check for ca.pem + let ca_pem_path = tls_dir.join("ca.pem"); + if !ca_pem_path.exists() { + let err_msg = format!("CA certificate file not found: {}", ca_pem_path.display()); + tracing::error!("{}", err_msg); + return Err(anyhow!(err_msg)); + } + + // Check for client.pem + let client_pem_path = tls_dir.join("client.pem"); + if !client_pem_path.exists() { + let err_msg = format!( + "Client certificate file not found: {}", + client_pem_path.display() + ); + tracing::error!("{}", err_msg); + return Err(anyhow!(err_msg)); + } + + // Check for client.key + let client_key_path = tls_dir.join("client.key"); + if !client_key_path.exists() { + let err_msg = format!("Client key file not found: {}", client_key_path.display()); + tracing::error!("{}", err_msg); + return Err(anyhow!(err_msg)); + } + + let server_root_ca_cert = std::fs::read_to_string(&ca_pem_path)?; + let server_root_ca_cert = Certificate::from_pem(server_root_ca_cert); + let client_cert = std::fs::read_to_string(&client_pem_path)?; + let client_key = std::fs::read_to_string(&client_key_path)?; + let client_identity = Identity::from_pem(client_cert, client_key); + let tls = ClientTlsConfig::new() + .ca_certificate(server_root_ca_cert) + .identity(client_identity); + + Channel::from_shared(addr)? + .tls_config(tls)? + .connect() + .await? + } else { + // No TLS directory, skip TLS configuration + Channel::from_shared(addr)?.connect().await? + }; + + let client = CdkPaymentProcessorClient::new(channel); + + Ok(Self { + inner: client, + wait_incoming_payment_stream_is_active: Arc::new(AtomicBool::new(false)), + cancel_incoming_payment_listener: CancellationToken::new(), + }) + } +} + +#[async_trait] +impl MintPayment for PaymentProcessorClient { + type Err = cdk_common::payment::Error; + + async fn get_settings(&self) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .get_settings(Request::new(SettingsRequest {})) + .await + .map_err(|err| { + tracing::error!("Could not get settings: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })?; + + let settings = response.into_inner(); + + Ok(serde_json::from_str(&settings.inner)?) + } + + /// Create a new invoice + async fn create_incoming_payment_request( + &self, + amount: Amount, + unit: &CurrencyUnit, + description: String, + unix_expiry: Option, + ) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .create_payment(Request::new(CreatePaymentRequest { + amount: amount.into(), + unit: unit.to_string(), + description, + unix_expiry, + })) + .await + .map_err(|err| { + tracing::error!("Could not create payment request: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })?; + + let response = response.into_inner(); + + Ok(response.try_into().map_err(|_| { + cdk_common::payment::Error::Anyhow(anyhow!("Could not create create payment response")) + })?) + } + + async fn get_payment_quote( + &self, + request: &str, + unit: &CurrencyUnit, + options: Option, + ) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .get_payment_quote(Request::new(super::PaymentQuoteRequest { + request: request.to_string(), + unit: unit.to_string(), + options: options.map(|o| o.into()), + })) + .await + .map_err(|err| { + tracing::error!("Could not get payment quote: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })?; + + let response = response.into_inner(); + + Ok(response.into()) + } + + async fn make_payment( + &self, + melt_quote: mint::MeltQuote, + partial_amount: Option, + max_fee_amount: Option, + ) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .make_payment(Request::new(MakePaymentRequest { + melt_quote: Some(melt_quote.into()), + partial_amount: partial_amount.map(|a| a.into()), + max_fee_amount: max_fee_amount.map(|a| a.into()), + })) + .await + .map_err(|err| { + tracing::error!("Could not pay payment request: {}", err); + + if err.message().contains("already paid") { + cdk_common::payment::Error::InvoiceAlreadyPaid + } else if err.message().contains("pending") { + cdk_common::payment::Error::InvoicePaymentPending + } else { + cdk_common::payment::Error::Custom(err.to_string()) + } + })?; + + let response = response.into_inner(); + + Ok(response.try_into().map_err(|_err| { + cdk_common::payment::Error::Anyhow(anyhow!("could not make payment")) + })?) + } + + /// Listen for invoices to be paid to the mint + #[instrument(skip_all)] + async fn wait_any_incoming_payment( + &self, + ) -> Result + Send>>, Self::Err> { + self.wait_incoming_payment_stream_is_active + .store(true, Ordering::SeqCst); + tracing::debug!("Client waiting for payment"); + let mut inner = self.inner.clone(); + let stream = inner + .wait_incoming_payment(WaitIncomingPaymentRequest {}) + .await + .map_err(|err| { + tracing::error!("Could not check incoming payment stream: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })? + .into_inner(); + + let cancel_token = self.cancel_incoming_payment_listener.clone(); + let cancel_fut = cancel_token.cancelled_owned(); + let active_flag = self.wait_incoming_payment_stream_is_active.clone(); + + let transformed_stream = stream + .take_until(cancel_fut) + .filter_map(|item| async move { + match item { + Ok(value) => { + tracing::warn!("{}", value.lookup_id); + Some(value.lookup_id) + } + Err(e) => { + tracing::error!("Error in payment stream: {}", e); + None // Skip this item and continue with the stream + } + } + }) + .inspect(move |_| { + active_flag.store(false, Ordering::SeqCst); + tracing::info!("Payment stream inactive"); + }); + + Ok(Box::pin(transformed_stream)) + } + + /// Is wait invoice active + fn is_wait_invoice_active(&self) -> bool { + self.wait_incoming_payment_stream_is_active + .load(Ordering::SeqCst) + } + + /// Cancel wait invoice + fn cancel_wait_invoice(&self) { + self.cancel_incoming_payment_listener.cancel(); + } + + async fn check_incoming_payment_status( + &self, + request_lookup_id: &str, + ) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .check_incoming_payment(Request::new(CheckIncomingPaymentRequest { + request_lookup_id: request_lookup_id.to_string(), + })) + .await + .map_err(|err| { + tracing::error!("Could not check incoming payment: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })?; + + let check_incoming = response.into_inner(); + + let status = check_incoming.status().as_str_name(); + + Ok(MintQuoteState::from_str(status)?) + } + + async fn check_outgoing_payment( + &self, + request_lookup_id: &str, + ) -> Result { + let mut inner = self.inner.clone(); + let response = inner + .check_outgoing_payment(Request::new(CheckOutgoingPaymentRequest { + request_lookup_id: request_lookup_id.to_string(), + })) + .await + .map_err(|err| { + tracing::error!("Could not check outgoing payment: {}", err); + cdk_common::payment::Error::Custom(err.to_string()) + })?; + + let check_outgoing = response.into_inner(); + + Ok(check_outgoing + .try_into() + .map_err(|_| cdk_common::payment::Error::UnknownPaymentState)?) + } +} diff --git a/crates/cdk-payment-processor/src/proto/mod.rs b/crates/cdk-payment-processor/src/proto/mod.rs new file mode 100644 index 00000000..6c26cab6 --- /dev/null +++ b/crates/cdk-payment-processor/src/proto/mod.rs @@ -0,0 +1,207 @@ +use std::str::FromStr; + +use cdk_common::payment::{ + CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse, +}; +use cdk_common::{Bolt11Invoice, CurrencyUnit, MeltQuoteBolt11Request}; +use melt_options::Options; +mod client; +mod server; + +pub use client::PaymentProcessorClient; +pub use server::PaymentProcessorServer; + +tonic::include_proto!("cdk_payment_processor"); + +impl TryFrom for CdkMakePaymentResponse { + type Error = crate::error::Error; + fn try_from(value: MakePaymentResponse) -> Result { + Ok(Self { + payment_lookup_id: value.payment_lookup_id.clone(), + payment_proof: value.payment_proof.clone(), + status: value.status().as_str_name().parse()?, + total_spent: value.total_spent.into(), + unit: value.unit.parse()?, + }) + } +} + +impl From for MakePaymentResponse { + fn from(value: CdkMakePaymentResponse) -> Self { + Self { + payment_lookup_id: value.payment_lookup_id.clone(), + payment_proof: value.payment_proof.clone(), + status: QuoteState::from(value.status).into(), + total_spent: value.total_spent.into(), + unit: value.unit.to_string(), + } + } +} + +impl From for CreatePaymentResponse { + fn from(value: CreateIncomingPaymentResponse) -> Self { + Self { + request_lookup_id: value.request_lookup_id, + request: value.request.to_string(), + expiry: value.expiry, + } + } +} + +impl TryFrom for CreateIncomingPaymentResponse { + type Error = crate::error::Error; + + fn try_from(value: CreatePaymentResponse) -> Result { + Ok(Self { + request_lookup_id: value.request_lookup_id, + request: value.request, + expiry: value.expiry, + }) + } +} + +impl From<&MeltQuoteBolt11Request> for PaymentQuoteRequest { + fn from(value: &MeltQuoteBolt11Request) -> Self { + Self { + request: value.request.to_string(), + unit: value.unit.to_string(), + options: value.options.map(|o| o.into()), + } + } +} + +impl From for PaymentQuoteResponse { + fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self { + Self { + request_lookup_id: value.request_lookup_id, + amount: value.amount.into(), + fee: value.fee.into(), + state: QuoteState::from(value.state).into(), + } + } +} + +impl From for MeltOptions { + fn from(value: cdk_common::nut05::MeltOptions) -> Self { + Self { + options: Some(value.into()), + } + } +} + +impl From for Options { + fn from(value: cdk_common::nut05::MeltOptions) -> Self { + match value { + cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp { + amount: mpp.amount.into(), + }), + } + } +} + +impl From for cdk_common::nut05::MeltOptions { + fn from(value: MeltOptions) -> Self { + let options = value.options.expect("option defined"); + match options { + Options::Mpp(mpp) => cdk_common::MeltOptions::new_mpp(mpp.amount), + } + } +} + +impl From for cdk_common::payment::PaymentQuoteResponse { + fn from(value: PaymentQuoteResponse) -> Self { + Self { + request_lookup_id: value.request_lookup_id.clone(), + amount: value.amount.into(), + fee: value.fee.into(), + state: value.state().into(), + } + } +} + +impl From for cdk_common::nut05::QuoteState { + fn from(value: QuoteState) -> Self { + match value { + QuoteState::Unpaid => Self::Unpaid, + QuoteState::Paid => Self::Paid, + QuoteState::Pending => Self::Pending, + QuoteState::Unknown => Self::Unknown, + QuoteState::Failed => Self::Failed, + QuoteState::Issued => Self::Unknown, + } + } +} + +impl From for QuoteState { + fn from(value: cdk_common::nut05::QuoteState) -> Self { + match value { + cdk_common::MeltQuoteState::Unpaid => Self::Unpaid, + cdk_common::MeltQuoteState::Paid => Self::Paid, + cdk_common::MeltQuoteState::Pending => Self::Pending, + cdk_common::MeltQuoteState::Unknown => Self::Unknown, + cdk_common::MeltQuoteState::Failed => Self::Failed, + } + } +} + +impl From for QuoteState { + fn from(value: cdk_common::nut04::QuoteState) -> Self { + match value { + cdk_common::MintQuoteState::Unpaid => Self::Unpaid, + cdk_common::MintQuoteState::Paid => Self::Paid, + cdk_common::MintQuoteState::Pending => Self::Pending, + cdk_common::MintQuoteState::Issued => Self::Issued, + } + } +} + +impl From for MeltQuote { + fn from(value: cdk_common::mint::MeltQuote) -> Self { + Self { + id: value.id.to_string(), + unit: value.unit.to_string(), + amount: value.amount.into(), + request: value.request, + fee_reserve: value.fee_reserve.into(), + state: QuoteState::from(value.state).into(), + expiry: value.expiry, + payment_preimage: value.payment_preimage, + request_lookup_id: value.request_lookup_id, + msat_to_pay: value.msat_to_pay.map(|a| a.into()), + } + } +} + +impl TryFrom for cdk_common::mint::MeltQuote { + type Error = crate::error::Error; + + fn try_from(value: MeltQuote) -> Result { + Ok(Self { + id: value + .id + .parse() + .map_err(|_| crate::error::Error::InvalidId)?, + unit: value.unit.parse()?, + amount: value.amount.into(), + request: value.request.clone(), + fee_reserve: value.fee_reserve.into(), + state: cdk_common::nut05::QuoteState::from(value.state()), + expiry: value.expiry, + payment_preimage: value.payment_preimage, + request_lookup_id: value.request_lookup_id, + msat_to_pay: value.msat_to_pay.map(|a| a.into()), + }) + } +} + +impl TryFrom for MeltQuoteBolt11Request { + type Error = crate::error::Error; + + fn try_from(value: PaymentQuoteRequest) -> Result { + Ok(Self { + request: Bolt11Invoice::from_str(&value.request)?, + unit: CurrencyUnit::from_str(&value.unit)?, + options: value.options.map(|o| o.into()), + }) + } +} diff --git a/crates/cdk-payment-processor/src/proto/payment_processor.proto b/crates/cdk-payment-processor/src/proto/payment_processor.proto new file mode 100644 index 00000000..a476ca6a --- /dev/null +++ b/crates/cdk-payment-processor/src/proto/payment_processor.proto @@ -0,0 +1,113 @@ +syntax = "proto3"; + +package cdk_payment_processor; + +service CdkPaymentProcessor { + rpc GetSettings(SettingsRequest) returns (SettingsResponse) {} + rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {} + rpc GetPaymentQuote(PaymentQuoteRequest) returns (PaymentQuoteResponse) {} + rpc MakePayment(MakePaymentRequest) returns (MakePaymentResponse) {} + rpc CheckIncomingPayment(CheckIncomingPaymentRequest) returns (CheckIncomingPaymentResponse) {} + rpc CheckOutgoingPayment(CheckOutgoingPaymentRequest) returns (MakePaymentResponse) {} + rpc WaitIncomingPayment(WaitIncomingPaymentRequest) returns (stream WaitIncomingPaymentResponse) {} +} + +message SettingsRequest {} + +message SettingsResponse { + string inner = 1; +} + +message CreatePaymentRequest { + uint64 amount = 1; + string unit = 2; + string description = 3; + optional uint64 unix_expiry = 4; +} + +message CreatePaymentResponse { + string request_lookup_id = 1; + string request = 2; + optional uint64 expiry = 3; +} + +message Mpp { + uint64 amount = 1; +} + +message MeltOptions { + oneof options { + Mpp mpp = 1; + } +} + +message PaymentQuoteRequest { + string request = 1; + string unit = 2; + optional MeltOptions options = 3; +} + +enum QuoteState { + UNPAID = 0; + PAID = 1; + PENDING = 2; + UNKNOWN = 3; + FAILED = 4; + ISSUED = 5; +} + + +message PaymentQuoteResponse { + string request_lookup_id = 1; + uint64 amount = 2; + uint64 fee = 3; + QuoteState state = 4; +} + +message MeltQuote { + string id = 1; + string unit = 2; + uint64 amount = 3; + string request = 4; + uint64 fee_reserve = 5; + QuoteState state = 6; + uint64 expiry = 7; + optional string payment_preimage = 8; + string request_lookup_id = 9; + optional uint64 msat_to_pay = 10; +} + +message MakePaymentRequest { + MeltQuote melt_quote = 1; + optional uint64 partial_amount = 2; + optional uint64 max_fee_amount = 3; +} + +message MakePaymentResponse { + string payment_lookup_id = 1; + optional string payment_proof = 2; + QuoteState status = 3; + uint64 total_spent = 4; + string unit = 5; +} + +message CheckIncomingPaymentRequest { + string request_lookup_id = 1; +} + +message CheckIncomingPaymentResponse { + QuoteState status = 1; +} + +message CheckOutgoingPaymentRequest { + string request_lookup_id = 1; +} + + +message WaitIncomingPaymentRequest { +} + + +message WaitIncomingPaymentResponse { + string lookup_id = 1; +} diff --git a/crates/cdk-payment-processor/src/proto/server.rs b/crates/cdk-payment-processor/src/proto/server.rs new file mode 100644 index 00000000..962f9f34 --- /dev/null +++ b/crates/cdk-payment-processor/src/proto/server.rs @@ -0,0 +1,345 @@ +use std::net::SocketAddr; +use std::path::PathBuf; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use cdk_common::payment::MintPayment; +use futures::{Stream, StreamExt}; +use serde_json::Value; +use tokio::sync::{mpsc, Notify}; +use tokio::task::JoinHandle; +use tokio::time::{sleep, Instant}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig}; +use tonic::{async_trait, Request, Response, Status}; +use tracing::instrument; + +use super::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer}; +use crate::proto::*; + +type ResponseStream = + Pin> + Send>>; + +/// Payment Processor +#[derive(Clone)] +pub struct PaymentProcessorServer { + inner: Arc + Send + Sync>, + socket_addr: SocketAddr, + shutdown: Arc, + handle: Option>>>, +} + +impl PaymentProcessorServer { + pub fn new( + payment_processor: Arc + Send + Sync>, + addr: &str, + port: u16, + ) -> anyhow::Result { + let socket_addr = SocketAddr::new(addr.parse()?, port); + Ok(Self { + inner: payment_processor, + socket_addr, + shutdown: Arc::new(Notify::new()), + handle: None, + }) + } + + /// Start fake wallet grpc server + pub async fn start(&mut self, tls_dir: Option) -> anyhow::Result<()> { + tracing::info!("Starting RPC server {}", self.socket_addr); + + let server = match tls_dir { + Some(tls_dir) => { + tracing::info!("TLS configuration found, starting secure server"); + + // Check for server.pem + let server_pem_path = tls_dir.join("server.pem"); + if !server_pem_path.exists() { + let err_msg = format!( + "TLS certificate file not found: {}", + server_pem_path.display() + ); + tracing::error!("{}", err_msg); + return Err(anyhow::anyhow!(err_msg)); + } + + // Check for server.key + let server_key_path = tls_dir.join("server.key"); + if !server_key_path.exists() { + let err_msg = format!("TLS key file not found: {}", server_key_path.display()); + tracing::error!("{}", err_msg); + return Err(anyhow::anyhow!(err_msg)); + } + + // Check for ca.pem + let ca_pem_path = tls_dir.join("ca.pem"); + if !ca_pem_path.exists() { + let err_msg = + format!("CA certificate file not found: {}", ca_pem_path.display()); + tracing::error!("{}", err_msg); + return Err(anyhow::anyhow!(err_msg)); + } + + let cert = std::fs::read_to_string(&server_pem_path)?; + let key = std::fs::read_to_string(&server_key_path)?; + let client_ca_cert = std::fs::read_to_string(&ca_pem_path)?; + + let client_ca_cert = Certificate::from_pem(client_ca_cert); + let server_identity = Identity::from_pem(cert, key); + let tls_config = ServerTlsConfig::new() + .identity(server_identity) + .client_ca_root(client_ca_cert); + + Server::builder() + .tls_config(tls_config)? + .add_service(CdkPaymentProcessorServer::new(self.clone())) + } + None => { + tracing::warn!("No valid TLS configuration found, starting insecure server"); + Server::builder().add_service(CdkPaymentProcessorServer::new(self.clone())) + } + }; + + let shutdown = self.shutdown.clone(); + let addr = self.socket_addr; + + self.handle = Some(Arc::new(tokio::spawn(async move { + let server = server.serve_with_shutdown(addr, async { + shutdown.notified().await; + }); + + server.await?; + Ok(()) + }))); + + Ok(()) + } + + /// Stop fake wallet grpc server + pub async fn stop(&self) -> anyhow::Result<()> { + const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + + if let Some(handle) = &self.handle { + tracing::info!("Initiating server shutdown"); + self.shutdown.notify_waiters(); + + let start = Instant::now(); + + while !handle.is_finished() { + if start.elapsed() >= SHUTDOWN_TIMEOUT { + tracing::error!( + "Server shutdown timed out after {} seconds, aborting handle", + SHUTDOWN_TIMEOUT.as_secs() + ); + handle.abort(); + break; + } + sleep(Duration::from_millis(100)).await; + } + + if handle.is_finished() { + tracing::info!("Server shutdown completed successfully"); + } + } else { + tracing::info!("No server handle found, nothing to stop"); + } + + Ok(()) + } +} + +impl Drop for PaymentProcessorServer { + fn drop(&mut self) { + tracing::debug!("Dropping payment process server"); + self.shutdown.notify_one(); + } +} + +#[async_trait] +impl CdkPaymentProcessor for PaymentProcessorServer { + async fn get_settings( + &self, + _request: Request, + ) -> Result, Status> { + let settings: Value = self + .inner + .get_settings() + .await + .map_err(|_| Status::internal("Could not get settings"))?; + + Ok(Response::new(SettingsResponse { + inner: settings.to_string(), + })) + } + + async fn create_payment( + &self, + request: Request, + ) -> Result, Status> { + let CreatePaymentRequest { + amount, + unit, + description, + unix_expiry, + } = request.into_inner(); + + let unit = + CurrencyUnit::from_str(&unit).map_err(|_| Status::invalid_argument("Invalid unit"))?; + let invoice_response = self + .inner + .create_incoming_payment_request(amount.into(), &unit, description, unix_expiry) + .await + .map_err(|_| Status::internal("Could not create invoice"))?; + + Ok(Response::new(invoice_response.into())) + } + + async fn get_payment_quote( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let options: Option = + request.options.as_ref().map(|options| (*options).into()); + + let payment_quote = self + .inner + .get_payment_quote( + &request.request, + &CurrencyUnit::from_str(&request.unit) + .map_err(|_| Status::invalid_argument("Invalid currency unit"))?, + options, + ) + .await + .map_err(|err| { + tracing::error!("Could not get bolt11 melt quote: {}", err); + Status::internal("Could not get melt quote") + })?; + + Ok(Response::new(payment_quote.into())) + } + + async fn make_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let pay_invoice = self + .inner + .make_payment( + request + .melt_quote + .ok_or(Status::invalid_argument("Meltquote is required"))? + .try_into() + .map_err(|_err| Status::invalid_argument("Invalid melt quote"))?, + request.partial_amount.map(|a| a.into()), + request.max_fee_amount.map(|a| a.into()), + ) + .await + .map_err(|err| { + tracing::error!("Could not make payment: {}", err); + + match err { + cdk_common::payment::Error::InvoiceAlreadyPaid => { + Status::already_exists("Payment request already paid") + } + cdk_common::payment::Error::InvoicePaymentPending => { + Status::already_exists("Payment request pending") + } + _ => Status::internal("Could not pay invoice"), + } + })?; + + Ok(Response::new(pay_invoice.into())) + } + + async fn check_incoming_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let check_response = self + .inner + .check_incoming_payment_status(&request.request_lookup_id) + .await + .map_err(|_| Status::internal("Could not check incoming payment status"))?; + + Ok(Response::new(CheckIncomingPaymentResponse { + status: QuoteState::from(check_response).into(), + })) + } + + async fn check_outgoing_payment( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let check_response = self + .inner + .check_outgoing_payment(&request.request_lookup_id) + .await + .map_err(|_| Status::internal("Could not check incoming payment status"))?; + + Ok(Response::new(check_response.into())) + } + + type WaitIncomingPaymentStream = ResponseStream; + + // Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0) + #[allow(clippy::incompatible_msrv)] + #[instrument(skip_all)] + async fn wait_incoming_payment( + &self, + _request: Request, + ) -> Result, Status> { + tracing::debug!("Server waiting for payment stream"); + let (tx, rx) = mpsc::channel(128); + + let shutdown_clone = self.shutdown.clone(); + let ln = self.inner.clone(); + tokio::spawn(async move { + loop { + tokio::select! { + _ = shutdown_clone.notified() => { + tracing::info!("Shutdown signal received, stopping task for "); + ln.cancel_wait_invoice(); + break; + } + result = ln.wait_any_incoming_payment() => { + match result { + Ok(mut stream) => { + while let Some(request_lookup_id) = stream.next().await { + match tx.send(Result::<_, Status>::Ok(WaitIncomingPaymentResponse{lookup_id: request_lookup_id} )).await { + Ok(_) => { + // item (server response) was queued to be send to client + } + Err(item) => { + tracing::error!("Error adding incoming payment to stream: {}", item); + break; + } + } + } + } + Err(err) => { + tracing::warn!("Could not get invoice stream for {}", err); + + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + } + } + } + }); + + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::WaitIncomingPaymentStream + )) + } +} diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index 86972dc5..af3bd5cd 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; -use cdk_common::common::{LnKey, QuoteTTL}; +use cdk_common::common::{PaymentProcessorKey, QuoteTTL}; use cdk_common::database::{self, MintDatabase}; use cdk_common::dhke::hash_to_curve; use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; @@ -826,7 +826,7 @@ impl MintDatabase for MintRedbDatabase { async fn add_melt_request( &self, melt_request: MeltBolt11Request, - ln_key: LnKey, + ln_key: PaymentProcessorKey, ) -> Result<(), Self::Err> { let write_txn = self.db.begin_write().map_err(Error::from)?; let mut table = write_txn.open_table(MELT_REQUESTS).map_err(Error::from)?; @@ -847,7 +847,7 @@ impl MintDatabase for MintRedbDatabase { async fn get_melt_request( &self, quote_id: &Uuid, - ) -> Result, LnKey)>, Self::Err> { + ) -> Result, PaymentProcessorKey)>, Self::Err> { let read_txn = self.db.begin_read().map_err(Error::from)?; let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?; diff --git a/crates/cdk-sqlite/src/mint/memory.rs b/crates/cdk-sqlite/src/mint/memory.rs index a9d0609a..0031ad45 100644 --- a/crates/cdk-sqlite/src/mint/memory.rs +++ b/crates/cdk-sqlite/src/mint/memory.rs @@ -1,7 +1,7 @@ //! In-memory database that is provided by the `cdk-sqlite` crate, mainly for testing purposes. use std::collections::HashMap; -use cdk_common::common::LnKey; +use cdk_common::common::PaymentProcessorKey; use cdk_common::database::{self, MintDatabase}; use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs}; @@ -29,7 +29,7 @@ pub async fn new_with_state( melt_quotes: Vec, pending_proofs: Proofs, spent_proofs: Proofs, - melt_request: Vec<(MeltBolt11Request, LnKey)>, + melt_request: Vec<(MeltBolt11Request, PaymentProcessorKey)>, mint_info: MintInfo, ) -> Result { let db = empty().await?; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index d2728fb8..176164c9 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use async_trait::async_trait; use bitcoin::bip32::DerivationPath; -use cdk_common::common::{LnKey, QuoteTTL}; +use cdk_common::common::{PaymentProcessorKey, QuoteTTL}; use cdk_common::database::{self, MintDatabase}; use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::nut00::ProofsMethods; @@ -1285,7 +1285,7 @@ WHERE keyset_id=?; async fn add_melt_request( &self, melt_request: MeltBolt11Request, - ln_key: LnKey, + ln_key: PaymentProcessorKey, ) -> Result<(), Self::Err> { let mut transaction = self.pool.begin().await.map_err(Error::from)?; @@ -1328,7 +1328,7 @@ ON CONFLICT(id) DO UPDATE SET async fn get_melt_request( &self, quote_id: &Uuid, - ) -> Result, LnKey)>, Self::Err> { + ) -> Result, PaymentProcessorKey)>, Self::Err> { let mut transaction = self.pool.begin().await.map_err(Error::from)?; let rec = sqlx::query( @@ -1708,7 +1708,9 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result Result<(MeltBolt11Request, LnKey), Error> { +fn sqlite_row_to_melt_request( + row: SqliteRow, +) -> Result<(MeltBolt11Request, PaymentProcessorKey), Error> { let quote_id: Hyphenated = row.try_get("id").map_err(Error::from)?; let row_inputs: String = row.try_get("inputs").map_err(Error::from)?; let row_outputs: Option = row.try_get("outputs").map_err(Error::from)?; @@ -1721,7 +1723,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()), }; - let ln_key = LnKey { + let ln_key = PaymentProcessorKey { unit: CurrencyUnit::from_str(&row_unit)?, method: PaymentMethod::from_str(&row_method)?, }; diff --git a/crates/cdk/src/lib.rs b/crates/cdk/src/lib.rs index 0d7ed7c9..602f01b4 100644 --- a/crates/cdk/src/lib.rs +++ b/crates/cdk/src/lib.rs @@ -28,7 +28,7 @@ pub use cdk_common::{ }; #[cfg(feature = "mint")] #[doc(hidden)] -pub use cdk_common::{lightning as cdk_lightning, subscription}; +pub use cdk_common::{payment as cdk_payment, subscription}; pub mod fees; diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index f5592469..1913d6e7 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -6,18 +6,20 @@ use std::sync::Arc; use anyhow::anyhow; use bitcoin::bip32::DerivationPath; use cdk_common::database::{self, MintDatabase}; +use cdk_common::error::Error; +use cdk_common::payment::Bolt11Settings; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; use super::Nuts; use crate::amount::Amount; -use crate::cdk_lightning::{self, MintLightning}; +use crate::cdk_payment::{self, MintPayment}; use crate::mint::Mint; use crate::nuts::{ ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, MppMethodSettings, PaymentMethod, }; -use crate::types::LnKey; +use crate::types::PaymentProcessorKey; /// Cashu Mint #[derive(Default)] @@ -27,7 +29,9 @@ pub struct MintBuilder { /// Mint Storage backend localstore: Option + Send + Sync>>, /// Ln backends for mint - ln: Option + Send + Sync>>>, + ln: Option< + HashMap + Send + Sync>>, + >, seed: Option>, supported_units: HashMap, custom_paths: HashMap, @@ -119,25 +123,27 @@ impl MintBuilder { } /// Add ln backend - pub fn add_ln_backend( + pub async fn add_ln_backend( mut self, unit: CurrencyUnit, method: PaymentMethod, limits: MintMeltLimits, - ln_backend: Arc + Send + Sync>, - ) -> Self { - let ln_key = LnKey { + ln_backend: Arc + Send + Sync>, + ) -> Result { + let ln_key = PaymentProcessorKey { unit: unit.clone(), - method, + method: method.clone(), }; let mut ln = self.ln.unwrap_or_default(); - let settings = ln_backend.get_settings(); + let settings = ln_backend.get_settings().await?; + + let settings: Bolt11Settings = settings.try_into()?; if settings.mpp { let mpp_settings = MppMethodSettings { - method, + method: method.clone(), unit: unit.clone(), }; @@ -150,7 +156,7 @@ impl MintBuilder { if method == PaymentMethod::Bolt11 { let mint_method_settings = MintMethodSettings { - method, + method: method.clone(), unit: unit.clone(), min_amount: Some(limits.mint_min), max_amount: Some(limits.mint_max), @@ -161,7 +167,7 @@ impl MintBuilder { self.mint_info.nuts.nut04.disabled = false; let melt_method_settings = MeltMethodSettings { - method, + method: method.clone(), unit, min_amount: Some(limits.melt_min), max_amount: Some(limits.melt_max), @@ -179,7 +185,7 @@ impl MintBuilder { self.ln = Some(ln); - self + Ok(self) } /// Set pubkey diff --git a/crates/cdk/src/mint/ln.rs b/crates/cdk/src/mint/ln.rs index 701d0b6d..f6b43958 100644 --- a/crates/cdk/src/mint/ln.rs +++ b/crates/cdk/src/mint/ln.rs @@ -1,4 +1,4 @@ -use cdk_common::common::LnKey; +use cdk_common::common::PaymentProcessorKey; use cdk_common::MintQuoteState; use super::Mint; @@ -14,7 +14,7 @@ impl Mint { .await? .ok_or(Error::UnknownQuote)?; - let ln = match self.ln.get(&LnKey::new( + let ln = match self.ln.get(&PaymentProcessorKey::new( quote.unit.clone(), cdk_common::PaymentMethod::Bolt11, )) { @@ -27,7 +27,7 @@ impl Mint { }; let ln_status = ln - .check_incoming_invoice_status("e.request_lookup_id) + .check_incoming_payment_status("e.request_lookup_id) .await?; if ln_status != quote.state && quote.state != MintQuoteState::Issued { diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 3547a1a4..87ad883f 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -12,14 +12,14 @@ use super::{ Mint, PaymentMethod, PublicKey, State, }; use crate::amount::to_unit; -use crate::cdk_lightning::{MintLightning, PayInvoiceResponse}; +use crate::cdk_payment::{MakePaymentResponse, MintPayment}; use crate::mint::verification::Verification; use crate::mint::SigFlag; use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag}; use crate::nuts::MeltQuoteState; -use crate::types::LnKey; +use crate::types::PaymentProcessorKey; use crate::util::unix_time; -use crate::{cdk_lightning, ensure_cdk, Amount, Error}; +use crate::{cdk_payment, ensure_cdk, Amount, Error}; impl Mint { #[instrument(skip_all)] @@ -112,22 +112,32 @@ impl Mint { let ln = self .ln - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) + .get(&PaymentProcessorKey::new( + unit.clone(), + PaymentMethod::Bolt11, + )) .ok_or_else(|| { tracing::info!("Could not get ln backend for {}, bolt11 ", unit); Error::UnsupportedUnit })?; - let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| { - tracing::error!( - "Could not get payment quote for mint quote, {} bolt11, {}", - unit, - err - ); + let payment_quote = ln + .get_payment_quote( + &melt_request.request.to_string(), + &melt_request.unit, + melt_request.options, + ) + .await + .map_err(|err| { + tracing::error!( + "Could not get payment quote for mint quote, {} bolt11, {}", + unit, + err + ); - Error::UnsupportedUnit - })?; + Error::UnsupportedUnit + })?; // We only want to set the msats_to_pay of the melt quote if the invoice is amountless // or we want to ignore the amount and do an mpp payment @@ -385,9 +395,9 @@ impl Mint { ) -> Result, Error> { use std::sync::Arc; async fn check_payment_state( - ln: Arc + Send + Sync>, + ln: Arc + Send + Sync>, melt_quote: &MeltQuote, - ) -> anyhow::Result { + ) -> anyhow::Result { match ln .check_outgoing_payment(&melt_quote.request_lookup_id) .await @@ -464,10 +474,10 @@ impl Mint { _ => None, }; tracing::debug!("partial_amount: {:?}", partial_amount); - let ln = match self - .ln - .get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11)) - { + let ln = match self.ln.get(&PaymentProcessorKey::new( + quote.unit.clone(), + PaymentMethod::Bolt11, + )) { Some(ln) => ln, None => { tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit); @@ -480,7 +490,7 @@ impl Mint { }; let pre = match ln - .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve)) + .make_payment(quote.clone(), partial_amount, Some(quote.fee_reserve)) .await { Ok(pay) @@ -503,7 +513,7 @@ impl Mint { Err(err) => { // If the error is that the invoice was already paid we do not want to hold // hold the proofs as pending to we reset them and return an error. - if matches!(err, cdk_lightning::Error::InvoiceAlreadyPaid) { + if matches!(err, cdk_payment::Error::InvoiceAlreadyPaid) { tracing::debug!("Invoice already paid, resetting melt quote"); if let Err(err) = self.process_unpaid_melt(melt_request).await { tracing::error!("Could not reset melt quote state: {}", err); @@ -570,7 +580,7 @@ impl Mint { } } - (pre.payment_preimage, amount_spent) + (pre.payment_proof, amount_spent) } }; diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index ead7c89a..96adb06f 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -1,3 +1,4 @@ +use cdk_common::payment::Bolt11Settings; use tracing::instrument; use uuid::Uuid; @@ -7,7 +8,7 @@ use super::{ NotificationPayload, PaymentMethod, PublicKey, }; use crate::nuts::MintQuoteState; -use crate::types::LnKey; +use crate::types::PaymentProcessorKey; use crate::util::unix_time; use crate::{ensure_cdk, Amount, Error}; @@ -29,10 +30,10 @@ impl Mint { let is_above_max = settings .max_amount - .map_or(false, |max_amount| amount > max_amount); + .is_some_and(|max_amount| amount > max_amount); let is_below_min = settings .min_amount - .map_or(false, |min_amount| amount < min_amount); + .is_some_and(|min_amount| amount < min_amount); let is_out_of_range = is_above_max || is_below_min; ensure_cdk!( @@ -64,7 +65,10 @@ impl Mint { let ln = self .ln - .get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) + .get(&PaymentProcessorKey::new( + unit.clone(), + PaymentMethod::Bolt11, + )) .ok_or_else(|| { tracing::info!("Bolt11 mint request for unsupported unit"); @@ -75,17 +79,20 @@ impl Mint { let quote_expiry = unix_time() + mint_ttl; - if description.is_some() && !ln.get_settings().invoice_description { + let settings = ln.get_settings().await?; + let settings: Bolt11Settings = serde_json::from_value(settings)?; + + if description.is_some() && !settings.invoice_description { tracing::error!("Backend does not support invoice description"); return Err(Error::InvoiceDescriptionUnsupported); } let create_invoice_response = ln - .create_invoice( + .create_incoming_payment_request( amount, &unit, description.unwrap_or("".to_string()), - quote_expiry, + Some(quote_expiry), ) .await .map_err(|err| { diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 88b1e27e..25f80518 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -5,18 +5,17 @@ use std::sync::Arc; use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::secp256k1::{self, Secp256k1}; -use cdk_common::common::{LnKey, QuoteTTL}; +use cdk_common::common::{PaymentProcessorKey, QuoteTTL}; use cdk_common::database::{self, MintDatabase}; use cdk_common::mint::MintKeySetInfo; use futures::StreamExt; -use serde::{Deserialize, Serialize}; use subscription::PubSubManager; use tokio::sync::{Notify, RwLock}; use tokio::task::JoinSet; use tracing::instrument; use uuid::Uuid; -use crate::cdk_lightning::{self, MintLightning}; +use crate::cdk_payment::{self, MintPayment}; use crate::dhke::{sign_message, verify_message}; use crate::error::Error; use crate::fees::calculate_fee; @@ -44,7 +43,8 @@ pub struct Mint { /// Mint Storage backend pub localstore: Arc + Send + Sync>, /// Ln backends for mint - pub ln: HashMap + Send + Sync>>, + pub ln: + HashMap + Send + Sync>>, /// Subscription manager pub pubsub_manager: Arc, secp_ctx: Secp256k1, @@ -59,7 +59,10 @@ impl Mint { pub async fn new( seed: &[u8], localstore: Arc + Send + Sync>, - ln: HashMap + Send + Sync>>, + ln: HashMap< + PaymentProcessorKey, + Arc + Send + Sync>, + >, // Hashmap where the key is the unit and value is (input fee ppk, max_order) supported_units: HashMap, custom_paths: HashMap, @@ -117,21 +120,25 @@ impl Mint { } /// Get mint info + #[instrument(skip_all)] pub async fn mint_info(&self) -> Result { Ok(self.localstore.get_mint_info().await?) } /// Set mint info + #[instrument(skip_all)] pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> { Ok(self.localstore.set_mint_info(mint_info).await?) } /// Get quote ttl + #[instrument(skip_all)] pub async fn quote_ttl(&self) -> Result { Ok(self.localstore.get_quote_ttl().await?) } /// Set quote ttl + #[instrument(skip_all)] pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> { Ok(self.localstore.set_quote_ttl(quote_ttl).await?) } @@ -139,6 +146,7 @@ impl Mint { /// Wait for any invoice to be paid /// For each backend starts a task that waits for any invoice to be paid /// Once invoice is paid mint quote status is updated + #[instrument(skip_all)] pub async fn wait_for_paid_invoices(&self, shutdown: Arc) -> Result<(), Error> { let mint_arc = Arc::new(self.clone()); @@ -146,19 +154,21 @@ impl Mint { for (key, ln) in self.ln.iter() { if !ln.is_wait_invoice_active() { + tracing::info!("Wait payment for {:?} inactive starting.", key); let mint = Arc::clone(&mint_arc); let ln = Arc::clone(ln); let shutdown = Arc::clone(&shutdown); let key = key.clone(); join_set.spawn(async move { loop { + tracing::info!("Restarting wait for: {:?}", key); tokio::select! { _ = shutdown.notified() => { tracing::info!("Shutdown signal received, stopping task for {:?}", key); ln.cancel_wait_invoice(); break; } - result = ln.wait_any_invoice() => { + result = ln.wait_any_incoming_payment() => { match result { Ok(mut stream) => { while let Some(request_lookup_id) = stream.next().await { @@ -168,7 +178,7 @@ impl Mint { } } Err(err) => { - tracing::warn!("Could not get invoice stream for {:?}: {}",key, err); + tracing::warn!("Could not get incoming payment stream for {:?}: {}",key, err); tokio::time::sleep(std::time::Duration::from_secs(5)).await; } @@ -432,15 +442,6 @@ impl Mint { } } -/// Mint Fee Reserve -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct FeeReserve { - /// Absolute expected min fee - pub min_fee_reserve: Amount, - /// Percentage expected fee - pub percent_fee_reserve: f32, -} - /// Generate new [`MintKeySetInfo`] from path #[instrument(skip_all)] fn create_new_keyset( @@ -490,7 +491,7 @@ mod tests { use std::str::FromStr; use bitcoin::Network; - use cdk_common::common::LnKey; + use cdk_common::common::PaymentProcessorKey; use cdk_sqlite::mint::memory::new_with_state; use secp256k1::Secp256k1; use uuid::Uuid; @@ -594,7 +595,7 @@ mod tests { seed: &'a [u8], mint_info: MintInfo, supported_units: HashMap, - melt_requests: Vec<(MeltBolt11Request, LnKey)>, + melt_requests: Vec<(MeltBolt11Request, PaymentProcessorKey)>, } async fn create_mint(config: MintConfig<'_>) -> Result { diff --git a/crates/cdk/src/mint/start_up_check.rs b/crates/cdk/src/mint/start_up_check.rs index 5dedd793..1f4a1d56 100644 --- a/crates/cdk/src/mint/start_up_check.rs +++ b/crates/cdk/src/mint/start_up_check.rs @@ -5,7 +5,7 @@ use super::{Error, Mint}; use crate::mint::{MeltQuote, MeltQuoteState, PaymentMethod}; -use crate::types::LnKey; +use crate::types::PaymentProcessorKey; impl Mint { /// Check the status of all pending mint quotes in the mint db @@ -38,7 +38,7 @@ impl Mint { let (melt_request, ln_key) = match melt_request_ln_key { None => { - let ln_key = LnKey { + let ln_key = PaymentProcessorKey { unit: pending_quote.unit, method: PaymentMethod::Bolt11, }; @@ -67,7 +67,7 @@ impl Mint { if let Err(err) = self .process_melt_request( &melt_request, - pay_invoice_response.payment_preimage, + pay_invoice_response.payment_proof, pay_invoice_response.total_spent, ) .await diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index cafc011e..6032823b 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -209,7 +209,7 @@ impl Wallet { .ok_or(Error::UnknownKeySet)? .input_fee_ppk; - let fee = (input_fee_ppk * count + 999) / 1000; + let fee = (input_fee_ppk * count).div_ceil(1000); Ok(Amount::from(fee)) } diff --git a/justfile b/justfile index 17b468ab..1a0ae7e3 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,3 @@ -import "./misc/justfile.custom.just" -import "./misc/test.just" - alias b := build alias c := check alias t := test @@ -66,3 +63,102 @@ typos: [no-exit-message] typos-fix: just typos -w + +itest db: + #!/usr/bin/env bash + ./misc/itests.sh "{{db}}" + + +fake-mint-itest db: + #!/usr/bin/env bash + ./misc/fake_itests.sh "{{db}}" + + +itest-payment-processor ln: + #!/usr/bin/env bash + ./misc/mintd_payment_processor.sh "{{ln}}" + +run-examples: + cargo r --example p2pk + cargo r --example mint-token + cargo r --example proof_selection + cargo r --example wallet + +check-wasm *ARGS="--target wasm32-unknown-unknown": + #!/usr/bin/env bash + set -euo pipefail + + if [ ! -f Cargo.toml ]; then + cd {{invocation_directory()}} + fi + + buildargs=( + "-p cdk" + "-p cdk --no-default-features" + "-p cdk --no-default-features --features wallet" + "-p cdk --no-default-features --features mint" + ) + + for arg in "${buildargs[@]}"; do + echo "Checking '$arg'" + cargo check $arg {{ARGS}} + echo + done + +release m="": + #!/usr/bin/env bash + set -euo pipefail + + args=( + "-p cashu" + "-p cdk-common" + "-p cdk" + "-p cdk-redb" + "-p cdk-sqlite" + "-p cdk-rexie" + "-p cdk-axum" + "-p cdk-mint-rpc" + "-p cdk-cln" + "-p cdk-lnd" + "-p cdk-strike" + "-p cdk-phoenixd" + "-p cdk-lnbits" + "-p cdk-fake-wallet" + "-p cdk-cli" + "-p cdk-mintd" + ) + + for arg in "${args[@]}"; + do + echo "Publishing '$arg'" + cargo publish $arg {{m}} + echo + done + +check-docs: + #!/usr/bin/env bash + set -euo pipefail + args=( + "-p cashu" + "-p cdk-common" + "-p cdk" + "-p cdk-redb" + "-p cdk-sqlite" + "-p cdk-axum" + "-p cdk-rexie" + "-p cdk-cln" + "-p cdk-lnd" + "-p cdk-strike" + "-p cdk-phoenixd" + "-p cdk-lnbits" + "-p cdk-fake-wallet" + "-p cdk-mint-rpc" + "-p cdk-cli" + "-p cdk-mintd" + ) + + for arg in "${args[@]}"; do + echo "Checking '$arg' docs" + cargo doc $arg --all-features + echo + done diff --git a/misc/mintd_payment_processor.sh b/misc/mintd_payment_processor.sh new file mode 100755 index 00000000..b6816daa --- /dev/null +++ b/misc/mintd_payment_processor.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +# Function to perform cleanup +cleanup() { + echo "Cleaning up..." + + + echo "Killing the cdk payment processor" + kill -2 $cdk_payment_processor_pid + wait $cdk_payment_processor_pid + + echo "Killing the cdk mintd" + kill -2 $cdk_mintd_pid + wait $cdk_mintd_pid + + echo "Killing the cdk regtest" + kill -2 $cdk_regtest_pid + wait $cdk_regtest_pid + + echo "Mint binary terminated" + + # Remove the temporary directory + rm -rf "$cdk_itests" + echo "Temp directory removed: $cdk_itests" + unset cdk_itests + unset cdk_itests_mint_addr + unset cdk_itests_mint_port +} + +# Set up trap to call cleanup on script exit +trap cleanup EXIT + +# Create a temporary directory +export cdk_itests=$(mktemp -d) +export cdk_itests_mint_addr="127.0.0.1"; +export cdk_itests_mint_port_0=8086; + + +export LN_BACKEND="$1"; + +URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port_0/v1/info" +# Check if the temporary directory was created successfully +if [[ ! -d "$cdk_itests" ]]; then + echo "Failed to create temp directory" + exit 1 +fi + +echo "Temp directory created: $cdk_itests" +export MINT_DATABASE="$1"; + +cargo build -p cdk-integration-tests + + +if [ "$LN_BACKEND" != "FAKEWALLET" ]; then + cargo run --bin start_regtest & + cdk_regtest_pid=$! + mkfifo "$cdk_itests/progress_pipe" + rm -f "$cdk_itests/signal_received" # Ensure clean state + # Start reading from pipe in background + (while read line; do + case "$line" in + "checkpoint1") + echo "Reached first checkpoint" + touch "$cdk_itests/signal_received" + exit 0 + ;; + esac + done < "$cdk_itests/progress_pipe") & + # Wait for up to 120 seconds + for ((i=0; i<120; i++)); do + if [ -f "$cdk_itests/signal_received" ]; then + echo "break signal received" + break + fi + sleep 1 + done + echo "Regtest set up continuing" +fi + +# Start payment processor + + +export CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH="$cdk_itests/cln/one/regtest/lightning-rpc"; + +export CDK_PAYMENT_PROCESSOR_LND_ADDRESS="https://localhost:10010"; +export CDK_PAYMENT_PROCESSOR_LND_CERT_FILE="$cdk_itests/lnd/two/tls.cert"; +export CDK_PAYMENT_PROCESSOR_LND_MACAROON_FILE="$cdk_itests/lnd/two/data/chain/bitcoin/regtest/admin.macaroon"; + +export CDK_PAYMENT_PROCESSOR_LN_BACKEND=$LN_BACKEND; +export CDK_PAYMENT_PROCESSOR_LISTEN_HOST="127.0.0.1"; +export CDK_PAYMENT_PROCESSOR_LISTEN_PORT="8090"; + +echo "$CDK_PAYMENT_PROCESSOR_CLN_RPC_PATH" + +cargo run --bin cdk-payment-processor & + +cdk_payment_processor_pid=$! + +sleep 10; + +export CDK_MINTD_URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port_0"; +export CDK_MINTD_WORK_DIR="$cdk_itests"; +export CDK_MINTD_LISTEN_HOST=$cdk_itests_mint_addr; +export CDK_MINTD_LISTEN_PORT=$cdk_itests_mint_port_0; +export CDK_MINTD_LN_BACKEND="grpcprocessor"; +export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_ADDRESS="http://127.0.0.1"; +export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_PORT="8090"; +export CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS="sat"; +export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal"; + +cargo run --bin cdk-mintd --no-default-features --features grpc-processor & +cdk_mintd_pid=$! + +echo $cdk_itests + +TIMEOUT=100 +START_TIME=$(date +%s) +# Loop until the endpoint returns a 200 OK status or timeout is reached +while true; do + # Get the current time + CURRENT_TIME=$(date +%s) + + # Calculate the elapsed time + ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) + + # Check if the elapsed time exceeds the timeout + if [ $ELAPSED_TIME -ge $TIMEOUT ]; then + echo "Timeout of $TIMEOUT seconds reached. Exiting..." + exit 1 + fi + + # Make a request to the endpoint and capture the HTTP status code + HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL) + + # Check if the HTTP status is 200 OK + if [ "$HTTP_STATUS" -eq 200 ]; then + echo "Received 200 OK from $URL" + break + else + echo "Waiting for 200 OK response, current status: $HTTP_STATUS" + sleep 2 # Wait for 2 seconds before retrying + fi +done + + +cargo test -p cdk-integration-tests --test payment_processor + +# Run cargo test +# cargo test -p cdk-integration-tests --test fake_wallet +# Capture the exit status of cargo test +test_status=$? + +# Exit with the status of the tests +exit $test_status diff --git a/misc/test.just b/misc/test.just deleted file mode 100644 index 8b48df95..00000000 --- a/misc/test.just +++ /dev/null @@ -1,9 +0,0 @@ -itest db: - #!/usr/bin/env bash - ./misc/itests.sh "{{db}}" - - -fake-mint-itest db: - #!/usr/bin/env bash - ./misc/fake_itests.sh "{{db}}" -