feat: payment processor

This commit is contained in:
thesimplekid
2025-02-11 13:36:43 +00:00
parent 1131711d91
commit 162507c492
57 changed files with 2460 additions and 357 deletions

View File

@@ -104,6 +104,7 @@ jobs:
-p cdk-lnd, -p cdk-lnd,
-p cdk-lnbits, -p cdk-lnbits,
-p cdk-fake-wallet, -p cdk-fake-wallet,
-p cdk-payment-processor,
--bin cdk-cli, --bin cdk-cli,
--bin cdk-cli --features sqlcipher, --bin cdk-cli --features sqlcipher,
--bin cdk-mintd, --bin cdk-mintd,
@@ -115,9 +116,11 @@ jobs:
--bin cdk-mintd --no-default-features --features cln, --bin cdk-mintd --no-default-features --features cln,
--bin cdk-mintd --no-default-features --features lnbits, --bin cdk-mintd --no-default-features --features lnbits,
--bin cdk-mintd --no-default-features --features fakewallet, --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 lnd",
--bin cdk-mintd --no-default-features --features "management-rpc cln", --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 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 lnd",
--bin cdk-mintd --no-default-features --features "swagger cln", --bin cdk-mintd --no-default-features --features "swagger cln",
--bin cdk-mintd --no-default-features --features "swagger lnbits", --bin cdk-mintd --no-default-features --features "swagger lnbits",
@@ -211,6 +214,30 @@ jobs:
- name: Test fake mint - name: Test fake mint
run: nix develop -i -L .#stable --command just test 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: msrv-build:
name: "MSRV build" name: "MSRV build"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -231,6 +258,7 @@ jobs:
-p cdk-mint-rpc, -p cdk-mint-rpc,
-p cdk-sqlite, -p cdk-sqlite,
-p cdk-mintd, -p cdk-mintd,
-p cdk-payment-processor --no-default-features,
] ]
steps: steps:
- name: checkout - name: checkout

View File

@@ -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-lnbits = { path = "./crates/cdk-lnbits", version = "=0.7.1" }
cdk-lnd = { path = "./crates/cdk-lnd", 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-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-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-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" } 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 } tokio-util = { version = "0.7.11", default-features = false }
tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] } tower-http = { version = "0.6.1", features = ["compression-full", "decompression-full", "cors", "trace"] }
tokio-tungstenite = { version = "0.26.0", default-features = false } 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 = { version = "0.1", default-features = false, features = ["attributes", "log"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = "2.3" url = "2.3"
@@ -63,6 +65,14 @@ once_cell = "1.20.2"
instant = { version = "0.1", default-features = false } instant = { version = "0.1", default-features = false }
rand = "0.8.5" rand = "0.8.5"
home = "0.5.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] [workspace.metadata]

View File

@@ -455,20 +455,22 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
/// Payment Method /// Payment Method
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum PaymentMethod { pub enum PaymentMethod {
/// Bolt11 payment type /// Bolt11 payment type
#[default] #[default]
Bolt11, Bolt11,
/// Custom
Custom(String),
} }
impl FromStr for PaymentMethod { impl FromStr for PaymentMethod {
type Err = Error; type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> { fn from_str(value: &str) -> Result<Self, Self::Err> {
match value { match value.to_lowercase().as_str() {
"bolt11" => Ok(Self::Bolt11), "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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
PaymentMethod::Bolt11 => write!(f, "bolt11"), PaymentMethod::Bolt11 => write!(f, "bolt11"),
PaymentMethod::Custom(p) => write!(f, "{}", p),
} }
} }
} }

View File

@@ -20,3 +20,4 @@ tokio-util.workspace = true
tracing.workspace = true tracing.workspace = true
thiserror.workspace = true thiserror.workspace = true
uuid.workspace = true uuid.workspace = true
serde_json.workspace = true

View File

@@ -28,7 +28,7 @@ pub enum Error {
Amount(#[from] cdk::amount::Error), Amount(#[from] cdk::amount::Error),
} }
impl From<Error> for cdk::cdk_lightning::Error { impl From<Error> for cdk::cdk_payment::Error {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
Self::Lightning(Box::new(e)) Self::Lightning(Box::new(e))
} }

View File

@@ -10,12 +10,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::amount::{to_unit, Amount};
use cdk::cdk_lightning::{ use cdk::cdk_payment::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
PaymentQuoteResponse,
}; };
use cdk::mint::FeeReserve; use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::types::FeeReserve;
use cdk::util::{hex, unix_time}; use cdk::util::{hex, unix_time};
use cdk::{mint, Bolt11Invoice}; use cdk::{mint, Bolt11Invoice};
use cln_rpc::model::requests::{ use cln_rpc::model::requests::{
@@ -28,6 +29,7 @@ use cln_rpc::model::responses::{
use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
use error::Error; use error::Error;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use serde_json::Value;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use uuid::Uuid; use uuid::Uuid;
@@ -60,15 +62,15 @@ impl Cln {
} }
#[async_trait] #[async_trait]
impl MintLightning for Cln { impl MintPayment for Cln {
type Err = cdk_lightning::Error; type Err = cdk_payment::Error;
fn get_settings(&self) -> Settings { async fn get_settings(&self) -> Result<Value, Self::Err> {
Settings { Ok(serde_json::to_value(Bolt11Settings {
mpp: true, mpp: true,
unit: CurrencyUnit::Msat, unit: CurrencyUnit::Msat,
invoice_description: true, invoice_description: true,
} })?)
} }
/// Is wait invoice active /// Is wait invoice active
@@ -81,7 +83,7 @@ impl MintLightning for Cln {
self.wait_invoice_cancel_token.cancel() self.wait_invoice_cancel_token.cancel()
} }
async fn wait_any_invoice( async fn wait_any_incoming_payment(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> { ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
let last_pay_index = self.get_last_pay_index().await?; let last_pay_index = self.get_last_pay_index().await?;
@@ -175,11 +177,21 @@ impl MintLightning for Cln {
async fn get_payment_quote( async fn get_payment_quote(
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, request: &str,
unit: &CurrencyUnit,
options: Option<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
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 = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -192,19 +204,19 @@ impl MintLightning for Cln {
}; };
Ok(PaymentQuoteResponse { Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.payment_hash().to_string(), request_lookup_id: bolt11.payment_hash().to_string(),
amount, amount,
fee: fee.into(), fee: fee.into(),
state: MeltQuoteState::Unpaid, state: MeltQuoteState::Unpaid,
}) })
} }
async fn pay_invoice( async fn make_payment(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>, partial_amount: Option<Amount>,
max_fee: Option<Amount>, max_fee: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
let pay_state = self let pay_state = self
.check_outgoing_payment(&bolt11.payment_hash().to_string()) .check_outgoing_payment(&bolt11.payment_hash().to_string())
@@ -271,8 +283,8 @@ impl MintLightning for Cln {
PayStatus::FAILED => MeltQuoteState::Failed, PayStatus::FAILED => MeltQuoteState::Failed,
}; };
PayInvoiceResponse { MakePaymentResponse {
payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
payment_lookup_id: pay_response.payment_hash.to_string(), payment_lookup_id: pay_response.payment_hash.to_string(),
status, status,
total_spent: to_unit( total_spent: to_unit(
@@ -292,15 +304,14 @@ impl MintLightning for Cln {
Ok(response) Ok(response)
} }
async fn create_invoice( async fn create_incoming_payment_request(
&self, &self,
amount: Amount, amount: Amount,
unit: &CurrencyUnit, unit: &CurrencyUnit,
description: String, description: String,
unix_expiry: u64, unix_expiry: Option<u64>,
) -> Result<CreateInvoiceResponse, Self::Err> { ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let time_now = unix_time(); let time_now = unix_time();
assert!(unix_expiry > time_now);
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
@@ -314,7 +325,7 @@ impl MintLightning for Cln {
amount_msat, amount_msat,
description, description,
label: label.clone(), label: label.clone(),
expiry: Some(unix_expiry - time_now), expiry: unix_expiry.map(|t| t - time_now),
fallbacks: None, fallbacks: None,
preimage: None, preimage: None,
cltv: None, cltv: None,
@@ -328,14 +339,14 @@ impl MintLightning for Cln {
let expiry = request.expires_at().map(|t| t.as_secs()); let expiry = request.expires_at().map(|t| t.as_secs());
let payment_hash = request.payment_hash(); let payment_hash = request.payment_hash();
Ok(CreateInvoiceResponse { Ok(CreateIncomingPaymentResponse {
request_lookup_id: payment_hash.to_string(), request_lookup_id: payment_hash.to_string(),
request, request: request.to_string(),
expiry, expiry,
}) })
} }
async fn check_incoming_invoice_status( async fn check_incoming_payment_status(
&self, &self,
payment_hash: &str, payment_hash: &str,
) -> Result<MintQuoteState, Self::Err> { ) -> Result<MintQuoteState, Self::Err> {
@@ -371,7 +382,7 @@ impl MintLightning for Cln {
async fn check_outgoing_payment( async fn check_outgoing_payment(
&self, &self,
payment_hash: &str, payment_hash: &str,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let listpays_response = cln_client let listpays_response = cln_client
@@ -390,9 +401,9 @@ impl MintLightning for Cln {
Some(pays_response) => { Some(pays_response) => {
let status = cln_pays_status_to_mint_state(pays_response.status); 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_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, status,
total_spent: pays_response total_spent: pays_response
.amount_sent_msat .amount_sent_msat
@@ -400,9 +411,9 @@ impl MintLightning for Cln {
unit: CurrencyUnit::Msat, unit: CurrencyUnit::Msat,
}) })
} }
None => Ok(PayInvoiceResponse { None => Ok(MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(), payment_lookup_id: payment_hash.to_string(),
payment_preimage: None, payment_proof: None,
status: MeltQuoteState::Unknown, status: MeltQuoteState::Unknown,
total_spent: Amount::ZERO, total_spent: Amount::ZERO,
unit: CurrencyUnit::Msat, unit: CurrencyUnit::Msat,

View File

@@ -143,14 +143,14 @@ impl ProofInfo {
/// Key used in hashmap of ln backends to identify what unit and payment method /// Key used in hashmap of ln backends to identify what unit and payment method
/// it is for /// it is for
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct LnKey { pub struct PaymentProcessorKey {
/// Unit of Payment backend /// Unit of Payment backend
pub unit: CurrencyUnit, pub unit: CurrencyUnit,
/// Method of payment backend /// Method of payment backend
pub method: PaymentMethod, pub method: PaymentMethod,
} }
impl LnKey { impl PaymentProcessorKey {
/// Create new [`LnKey`] /// Create new [`LnKey`]
pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self { pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
Self { unit, method } Self { unit, method }
@@ -241,3 +241,12 @@ mod tests {
assert_eq!(melted.total_amount(), Amount::from(32)); 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,
}

View File

@@ -7,7 +7,7 @@ use cashu::MintInfo;
use uuid::Uuid; use uuid::Uuid;
use super::Error; use super::Error;
use crate::common::{LnKey, QuoteTTL}; use crate::common::{PaymentProcessorKey, QuoteTTL};
use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote}; use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
use crate::nuts::{ use crate::nuts::{
BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
@@ -76,13 +76,13 @@ pub trait Database {
async fn add_melt_request( async fn add_melt_request(
&self, &self,
melt_request: MeltBolt11Request<Uuid>, melt_request: MeltBolt11Request<Uuid>,
ln_key: LnKey, ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err>; ) -> Result<(), Self::Err>;
/// Get melt request /// Get melt request
async fn get_melt_request( async fn get_melt_request(
&self, &self,
quote_id: &Uuid, quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err>; ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err>;
/// Add [`MintKeySetInfo`] /// Add [`MintKeySetInfo`]
async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>; async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;

View File

@@ -264,10 +264,10 @@ pub enum Error {
/// Database Error /// Database Error
#[error(transparent)] #[error(transparent)]
Database(#[from] crate::database::Error), Database(#[from] crate::database::Error),
/// Lightning Error /// Payment Error
#[error(transparent)] #[error(transparent)]
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
Lightning(#[from] crate::lightning::Error), Payment(#[from] crate::payment::Error),
} }
/// CDK Error Response /// CDK Error Response

View File

@@ -10,7 +10,7 @@ pub mod common;
pub mod database; pub mod database;
pub mod error; pub mod error;
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
pub mod lightning; pub mod payment;
pub mod pub_sub; pub mod pub_sub;
pub mod subscription; pub mod subscription;
pub mod ws; pub mod ws;

View File

@@ -3,12 +3,14 @@
use std::pin::Pin; use std::pin::Pin;
use async_trait::async_trait; use async_trait::async_trait;
use cashu::MeltOptions;
use futures::Stream; use futures::Stream;
use lightning_invoice::{Bolt11Invoice, ParseOrSemanticError}; use lightning_invoice::ParseOrSemanticError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error; use thiserror::Error;
use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
use crate::{mint, Amount}; use crate::{mint, Amount};
/// CDK Lightning Error /// CDK Lightning Error
@@ -23,6 +25,9 @@ pub enum Error {
/// Unsupported unit /// Unsupported unit
#[error("Unsupported unit")] #[error("Unsupported unit")]
UnsupportedUnit, UnsupportedUnit,
/// Unsupported payment option
#[error("Unsupported payment option")]
UnsupportedPaymentOption,
/// Payment state is unknown /// Payment state is unknown
#[error("Payment state is unknown")] #[error("Payment state is unknown")]
UnknownPaymentState, UnknownPaymentState,
@@ -41,47 +46,55 @@ pub enum Error {
/// Amount Error /// Amount Error
#[error(transparent)] #[error(transparent)]
Amount(#[from] crate::amount::Error), Amount(#[from] crate::amount::Error),
/// NUT04 Error
#[error(transparent)]
NUT04(#[from] crate::nuts::nut04::Error),
/// NUT05 Error /// NUT05 Error
#[error(transparent)] #[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error), NUT05(#[from] crate::nuts::nut05::Error),
/// Custom
#[error("`{0}`")]
Custom(String),
} }
/// MintLighting Trait /// Mint payment trait
#[async_trait] #[async_trait]
pub trait MintLightning { pub trait MintPayment {
/// Mint Lightning Error /// Mint Lightning Error
type Err: Into<Error> + From<Error>; type Err: Into<Error> + From<Error>;
/// Base Unit /// Base Settings
fn get_settings(&self) -> Settings; async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
/// Create a new invoice /// Create a new invoice
async fn create_invoice( async fn create_incoming_payment_request(
&self, &self,
amount: Amount, amount: Amount,
unit: &CurrencyUnit, unit: &CurrencyUnit,
description: String, description: String,
unix_expiry: u64, unix_expiry: Option<u64>,
) -> Result<CreateInvoiceResponse, Self::Err>; ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
/// Get payment quote /// Get payment quote
/// Used to get fee and amount required for a payment request /// Used to get fee and amount required for a payment request
async fn get_payment_quote( async fn get_payment_quote(
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, request: &str,
unit: &CurrencyUnit,
options: Option<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err>; ) -> Result<PaymentQuoteResponse, Self::Err>;
/// Pay bolt11 invoice /// Pay request
async fn pay_invoice( async fn make_payment(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>, partial_amount: Option<Amount>,
max_fee_amount: Option<Amount>, max_fee_amount: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err>; ) -> Result<MakePaymentResponse, Self::Err>;
/// Listen for invoices to be paid to the mint /// Listen for invoices to be paid to the mint
/// Returns a stream of request_lookup_id once invoices are paid /// Returns a stream of request_lookup_id once invoices are paid
async fn wait_any_invoice( async fn wait_any_incoming_payment(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>; ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
@@ -92,7 +105,7 @@ pub trait MintLightning {
fn cancel_wait_invoice(&self); fn cancel_wait_invoice(&self);
/// Check the status of an incoming payment /// Check the status of an incoming payment
async fn check_incoming_invoice_status( async fn check_incoming_payment_status(
&self, &self,
request_lookup_id: &str, request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err>; ) -> Result<MintQuoteState, Self::Err>;
@@ -101,27 +114,27 @@ pub trait MintLightning {
async fn check_outgoing_payment( async fn check_outgoing_payment(
&self, &self,
request_lookup_id: &str, request_lookup_id: &str,
) -> Result<PayInvoiceResponse, Self::Err>; ) -> Result<MakePaymentResponse, Self::Err>;
} }
/// Create invoice response /// Create incoming payment response
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateInvoiceResponse { pub struct CreateIncomingPaymentResponse {
/// Id that is used to look up the invoice from the ln backend /// Id that is used to look up the payment from the ln backend
pub request_lookup_id: String, pub request_lookup_id: String,
/// Bolt11 payment request /// Payment request
pub request: Bolt11Invoice, pub request: String,
/// Unix Expiry of Invoice /// Unix Expiry of Invoice
pub expiry: Option<u64>, pub expiry: Option<u64>,
} }
/// Pay invoice response /// Payment response
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayInvoiceResponse { pub struct MakePaymentResponse {
/// Payment hash /// Payment hash
pub payment_lookup_id: String, pub payment_lookup_id: String,
/// Payment Preimage /// Payment proof
pub payment_preimage: Option<String>, pub payment_proof: Option<String>,
/// Status /// Status
pub status: MeltQuoteState, pub status: MeltQuoteState,
/// Total Amount Spent /// Total Amount Spent
@@ -145,7 +158,7 @@ pub struct PaymentQuoteResponse {
/// Ln backend settings /// Ln backend settings
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Settings { pub struct Bolt11Settings {
/// MPP supported /// MPP supported
pub mpp: bool, pub mpp: bool,
/// Base unit of backend /// Base unit of backend
@@ -153,3 +166,19 @@ pub struct Settings {
/// Invoice Description supported /// Invoice Description supported
pub invoice_description: bool, pub invoice_description: bool,
} }
impl TryFrom<Bolt11Settings> for Value {
type Error = crate::error::Error;
fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
serde_json::to_value(value).map_err(|err| err.into())
}
}
impl TryFrom<Value> for Bolt11Settings {
type Error = crate::error::Error;
fn try_from(value: Value) -> Result<Self, Self::Error> {
serde_json::from_value(value).map_err(|err| err.into())
}
}

View File

@@ -21,4 +21,4 @@ thiserror.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
lightning-invoice.workspace = true lightning-invoice.workspace = true
tokio-stream = "0.1.15" tokio-stream.workspace = true

View File

@@ -16,7 +16,7 @@ pub enum Error {
NoReceiver, NoReceiver,
} }
impl From<Error> for cdk::cdk_lightning::Error { impl From<Error> for cdk::cdk_payment::Error {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
Self::Lightning(Box::new(e)) Self::Lightning(Box::new(e))
} }

View File

@@ -15,23 +15,25 @@ use async_trait::async_trait;
use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::rand::{thread_rng, Rng}; use bitcoin::secp256k1::rand::{thread_rng, Rng};
use bitcoin::secp256k1::{Secp256k1, SecretKey}; use bitcoin::secp256k1::{Secp256k1, SecretKey};
use cdk::amount::{Amount, MSAT_IN_SAT}; use cdk::amount::{to_unit, Amount};
use cdk::cdk_lightning::{ use cdk::cdk_payment::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
PaymentQuoteResponse,
}; };
use cdk::mint::FeeReserve; use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::types::FeeReserve;
use cdk::util::unix_time;
use cdk::{ensure_cdk, mint}; use cdk::{ensure_cdk, mint};
use error::Error; use error::Error;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use futures::Stream; use futures::Stream;
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret}; use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time; use tokio::time;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::instrument;
pub mod error; pub mod error;
@@ -49,7 +51,7 @@ pub struct FakeWallet {
} }
impl FakeWallet { impl FakeWallet {
/// Creat new [`FakeWallet`] /// Create new [`FakeWallet`]
pub fn new( pub fn new(
fee_reserve: FeeReserve, fee_reserve: FeeReserve,
payment_states: HashMap<String, MeltQuoteState>, payment_states: HashMap<String, MeltQuoteState>,
@@ -96,40 +98,56 @@ impl Default for FakeInvoiceDescription {
} }
#[async_trait] #[async_trait]
impl MintLightning for FakeWallet { impl MintPayment for FakeWallet {
type Err = cdk_lightning::Error; type Err = cdk_payment::Error;
fn get_settings(&self) -> Settings { #[instrument(skip_all)]
Settings { async fn get_settings(&self) -> Result<Value, Self::Err> {
Ok(serde_json::to_value(Bolt11Settings {
mpp: true, mpp: true,
unit: CurrencyUnit::Msat, unit: CurrencyUnit::Msat,
invoice_description: true, invoice_description: true,
} })?)
} }
#[instrument(skip_all)]
fn is_wait_invoice_active(&self) -> bool { fn is_wait_invoice_active(&self) -> bool {
self.wait_invoice_is_active.load(Ordering::SeqCst) self.wait_invoice_is_active.load(Ordering::SeqCst)
} }
#[instrument(skip_all)]
fn cancel_wait_invoice(&self) { fn cancel_wait_invoice(&self) {
self.wait_invoice_cancel_token.cancel() self.wait_invoice_cancel_token.cancel()
} }
async fn wait_any_invoice( #[instrument(skip_all)]
async fn wait_any_incoming_payment(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> { ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
tracing::info!("Starting stream for fake invoices");
let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?; let receiver = self.receiver.lock().await.take().ok_or(Error::NoReceiver)?;
let receiver_stream = ReceiverStream::new(receiver); let receiver_stream = ReceiverStream::new(receiver);
Ok(Box::pin(receiver_stream.map(|label| label))) Ok(Box::pin(receiver_stream.map(|label| label)))
} }
#[instrument(skip_all)]
async fn get_payment_quote( async fn get_payment_quote(
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, request: &str,
unit: &CurrencyUnit,
options: Option<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
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 = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -142,19 +160,20 @@ impl MintLightning for FakeWallet {
}; };
Ok(PaymentQuoteResponse { Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.payment_hash().to_string(), request_lookup_id: bolt11.payment_hash().to_string(),
amount, amount,
fee: fee.into(), fee: fee.into(),
state: MeltQuoteState::Unpaid, state: MeltQuoteState::Unpaid,
}) })
} }
async fn pay_invoice( #[instrument(skip_all)]
async fn make_payment(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
_partial_msats: Option<Amount>, _partial_msats: Option<Amount>,
_max_fee_msats: Option<Amount>, _max_fee_msats: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?; let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
let payment_hash = bolt11.payment_hash().to_string(); let payment_hash = bolt11.payment_hash().to_string();
@@ -185,8 +204,8 @@ impl MintLightning for FakeWallet {
ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into()); ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
} }
Ok(PayInvoiceResponse { Ok(MakePaymentResponse {
payment_preimage: Some("".to_string()), payment_proof: Some("".to_string()),
payment_lookup_id: payment_hash, payment_lookup_id: payment_hash,
status: payment_status, status: payment_status,
total_spent: melt_quote.amount, 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, &self,
amount: Amount, amount: Amount,
_unit: &CurrencyUnit, _unit: &CurrencyUnit,
description: String, description: String,
unix_expiry: u64, _unix_expiry: Option<u64>,
) -> Result<CreateInvoiceResponse, Self::Err> { ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let time_now = unix_time();
assert!(unix_expiry > time_now);
// Since this is fake we just use the amount no matter the unit to create an invoice // Since this is fake we just use the amount no matter the unit to create an invoice
let amount_msat = amount; let amount_msat = amount;
@@ -229,24 +246,26 @@ impl MintLightning for FakeWallet {
let expiry = invoice.expires_at().map(|t| t.as_secs()); let expiry = invoice.expires_at().map(|t| t.as_secs());
Ok(CreateInvoiceResponse { Ok(CreateIncomingPaymentResponse {
request_lookup_id: payment_hash.to_string(), request_lookup_id: payment_hash.to_string(),
request: invoice, request: invoice.to_string(),
expiry, expiry,
}) })
} }
async fn check_incoming_invoice_status( #[instrument(skip_all)]
async fn check_incoming_payment_status(
&self, &self,
_request_lookup_id: &str, _request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err> { ) -> Result<MintQuoteState, Self::Err> {
Ok(MintQuoteState::Paid) Ok(MintQuoteState::Paid)
} }
#[instrument(skip_all)]
async fn check_outgoing_payment( async fn check_outgoing_payment(
&self, &self,
request_lookup_id: &str, request_lookup_id: &str,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
// For fake wallet if the state is not explicitly set default to paid // For fake wallet if the state is not explicitly set default to paid
let states = self.payment_states.lock().await; let states = self.payment_states.lock().await;
let status = states.get(request_lookup_id).cloned(); 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; let fail_payments = self.failed_payment_check.lock().await;
if fail_payments.contains(request_lookup_id) { if fail_payments.contains(request_lookup_id) {
return Err(cdk_lightning::Error::InvoicePaymentPending); return Err(cdk_payment::Error::InvoicePaymentPending);
} }
Ok(PayInvoiceResponse { Ok(MakePaymentResponse {
payment_preimage: Some("".to_string()), payment_proof: Some("".to_string()),
payment_lookup_id: request_lookup_id.to_string(), payment_lookup_id: request_lookup_id.to_string(),
status, status,
total_spent: Amount::ZERO, total_spent: Amount::ZERO,
unit: self.get_settings().unit, unit: CurrencyUnit::Msat,
}) })
} }
} }
/// Create fake invoice /// Create fake invoice
#[instrument]
pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice { pub fn create_fake_invoice(amount_msat: u64, description: String) -> Bolt11Invoice {
let private_key = SecretKey::from_slice( let private_key = SecretKey::from_slice(
&[ &[

View File

@@ -7,7 +7,7 @@ use async_trait::async_trait;
use bip39::Mnemonic; use bip39::Mnemonic;
use cdk::amount::SplitTarget; use cdk::amount::SplitTarget;
use cdk::cdk_database::MintDatabase; use cdk::cdk_database::MintDatabase;
use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::mint::{MintBuilder, MintMeltLimits};
use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{ use cdk::nuts::{
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
@@ -15,7 +15,7 @@ use cdk::nuts::{
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod, MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod,
RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
}; };
use cdk::types::QuoteTTL; use cdk::types::{FeeReserve, QuoteTTL};
use cdk::util::unix_time; use cdk::util::unix_time;
use cdk::wallet::client::MintConnector; use cdk::wallet::client::MintConnector;
use cdk::wallet::Wallet; use cdk::wallet::Wallet;
@@ -167,20 +167,22 @@ pub async fn create_and_start_test_mint() -> anyhow::Result<Arc<Mint>> {
percent_fee_reserve: 1.0, percent_fee_reserve: 1.0,
}; };
let ln_fake_backend = Arc::new(FakeWallet::new( let ln_fake_backend = FakeWallet::new(
fee_reserve.clone(), fee_reserve.clone(),
HashMap::default(), HashMap::default(),
HashSet::default(), HashSet::default(),
0, 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)?; let mnemonic = Mnemonic::generate(12)?;
mint_builder = mint_builder mint_builder = mint_builder

View File

@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use cdk::mint::FeeReserve; use cdk::types::FeeReserve;
use cdk_cln::Cln as CdkCln; use cdk_cln::Cln as CdkCln;
use cdk_lnd::Lnd as CdkLnd; use cdk_lnd::Lnd as CdkLnd;
use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoin_client::BitcoinClient;

View File

@@ -8,14 +8,14 @@ use bip39::Mnemonic;
use cdk::amount::{Amount, SplitTarget}; use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::MintDatabase; use cdk::cdk_database::MintDatabase;
use cdk::dhke::construct_proofs; 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::nut00::ProofsMethods;
use cdk::nuts::{ use cdk::nuts::{
CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod, CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod,
PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest,
}; };
use cdk::subscription::{IndexableParams, Params}; use cdk::subscription::{IndexableParams, Params};
use cdk::types::QuoteTTL; use cdk::types::{FeeReserve, QuoteTTL};
use cdk::util::unix_time; use cdk::util::unix_time;
use cdk::Mint; use cdk::Mint;
use cdk_fake_wallet::FakeWallet; use cdk_fake_wallet::FakeWallet;
@@ -439,12 +439,14 @@ async fn test_correct_keyset() -> Result<()> {
let localstore = Arc::new(database); let localstore = Arc::new(database);
mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.with_localstore(localstore.clone());
mint_builder = mint_builder.add_ln_backend( mint_builder = mint_builder
CurrencyUnit::Sat, .add_ln_backend(
PaymentMethod::Bolt11, CurrencyUnit::Sat,
MintMeltLimits::new(1, 5_000), PaymentMethod::Bolt11,
Arc::new(fake_wallet), MintMeltLimits::new(1, 5_000),
); Arc::new(fake_wallet),
)
.await?;
mint_builder = mint_builder mint_builder = mint_builder
.with_name("regtest mint".to_string()) .with_name("regtest mint".to_string())

View File

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

View File

@@ -21,3 +21,4 @@ tokio-util.workspace = true
tracing.workspace = true tracing.workspace = true
thiserror.workspace = true thiserror.workspace = true
lnbits-rs = "0.4.0" lnbits-rs = "0.4.0"
serde_json.workspace = true

View File

@@ -19,7 +19,7 @@ pub enum Error {
Anyhow(#[from] anyhow::Error), Anyhow(#[from] anyhow::Error),
} }
impl From<Error> for cdk::cdk_lightning::Error { impl From<Error> for cdk::cdk_payment::Error {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
Self::Lightning(Box::new(e)) Self::Lightning(Box::new(e))
} }

View File

@@ -4,6 +4,7 @@
#![warn(rustdoc::bare_urls)] #![warn(rustdoc::bare_urls)]
use std::pin::Pin; use std::pin::Pin;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -11,11 +12,12 @@ use anyhow::anyhow;
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
use cdk::cdk_lightning::{ use cdk::cdk_payment::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
PaymentQuoteResponse,
}; };
use cdk::mint::FeeReserve; use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState}; use cdk::types::FeeReserve;
use cdk::util::unix_time; use cdk::util::unix_time;
use cdk::{mint, Bolt11Invoice}; use cdk::{mint, Bolt11Invoice};
use error::Error; use error::Error;
@@ -23,6 +25,7 @@ use futures::stream::StreamExt;
use futures::Stream; use futures::Stream;
use lnbits_rs::api::invoice::CreateInvoiceRequest; use lnbits_rs::api::invoice::CreateInvoiceRequest;
use lnbits_rs::LNBitsClient; use lnbits_rs::LNBitsClient;
use serde_json::Value;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -37,6 +40,7 @@ pub struct LNbits {
webhook_url: String, webhook_url: String,
wait_invoice_cancel_token: CancellationToken, wait_invoice_cancel_token: CancellationToken,
wait_invoice_is_active: Arc<AtomicBool>, wait_invoice_is_active: Arc<AtomicBool>,
settings: Bolt11Settings,
} }
impl LNbits { impl LNbits {
@@ -59,20 +63,21 @@ impl LNbits {
webhook_url, webhook_url,
wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_cancel_token: CancellationToken::new(),
wait_invoice_is_active: Arc::new(AtomicBool::new(false)), wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
settings: Bolt11Settings {
mpp: false,
unit: CurrencyUnit::Sat,
invoice_description: true,
},
}) })
} }
} }
#[async_trait] #[async_trait]
impl MintLightning for LNbits { impl MintPayment for LNbits {
type Err = cdk_lightning::Error; type Err = cdk_payment::Error;
fn get_settings(&self) -> Settings { async fn get_settings(&self) -> Result<Value, Self::Err> {
Settings { Ok(serde_json::to_value(&self.settings)?)
mpp: false,
unit: CurrencyUnit::Sat,
invoice_description: true,
}
} }
fn is_wait_invoice_active(&self) -> bool { fn is_wait_invoice_active(&self) -> bool {
@@ -83,7 +88,7 @@ impl MintLightning for LNbits {
self.wait_invoice_cancel_token.cancel() self.wait_invoice_cancel_token.cancel()
} }
async fn wait_any_invoice( async fn wait_any_incoming_payment(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> { ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
let receiver = self let receiver = self
@@ -145,15 +150,30 @@ impl MintLightning for LNbits {
async fn get_payment_quote( async fn get_payment_quote(
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, request: &str,
unit: &CurrencyUnit,
options: Option<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
if melt_quote_request.unit != CurrencyUnit::Sat { if unit != &CurrencyUnit::Sat {
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); 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 = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -166,19 +186,19 @@ impl MintLightning for LNbits {
}; };
Ok(PaymentQuoteResponse { Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.payment_hash().to_string(), request_lookup_id: bolt11.payment_hash().to_string(),
amount, amount,
fee: fee.into(), fee: fee.into(),
state: MeltQuoteState::Unpaid, state: MeltQuoteState::Unpaid,
}) })
} }
async fn pay_invoice( async fn make_payment(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
_partial_msats: Option<Amount>, _partial_msats: Option<Amount>,
_max_fee_msats: Option<Amount>, _max_fee_msats: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let pay_response = self let pay_response = self
.lnbits_api .lnbits_api
.pay_invoice(&melt_quote.request, None) .pay_invoice(&melt_quote.request, None)
@@ -212,36 +232,35 @@ impl MintLightning for LNbits {
.unsigned_abs(), .unsigned_abs(),
); );
Ok(PayInvoiceResponse { Ok(MakePaymentResponse {
payment_lookup_id: pay_response.payment_hash, payment_lookup_id: pay_response.payment_hash,
payment_preimage: Some(invoice_info.payment_hash), payment_proof: Some(invoice_info.payment_hash),
status, status,
total_spent, total_spent,
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
}) })
} }
async fn create_invoice( async fn create_incoming_payment_request(
&self, &self,
amount: Amount, amount: Amount,
unit: &CurrencyUnit, unit: &CurrencyUnit,
description: String, description: String,
unix_expiry: u64, unix_expiry: Option<u64>,
) -> Result<CreateInvoiceResponse, Self::Err> { ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
if unit != &CurrencyUnit::Sat { if unit != &CurrencyUnit::Sat {
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
} }
let time_now = unix_time(); 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 { let invoice_request = CreateInvoiceRequest {
amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(), amount: to_unit(amount, unit, &CurrencyUnit::Sat)?.into(),
memo: Some(description), memo: Some(description),
unit: unit.to_string(), unit: unit.to_string(),
expiry: Some(expiry), expiry,
webhook: Some(self.webhook_url.clone()), webhook: Some(self.webhook_url.clone()),
internal: None, internal: None,
out: false, out: false,
@@ -260,14 +279,14 @@ impl MintLightning for LNbits {
let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?; let request: Bolt11Invoice = create_invoice_response.payment_request.parse()?;
let expiry = request.expires_at().map(|t| t.as_secs()); let expiry = request.expires_at().map(|t| t.as_secs());
Ok(CreateInvoiceResponse { Ok(CreateIncomingPaymentResponse {
request_lookup_id: create_invoice_response.payment_hash, request_lookup_id: create_invoice_response.payment_hash,
request, request: request.to_string(),
expiry, expiry,
}) })
} }
async fn check_incoming_invoice_status( async fn check_incoming_payment_status(
&self, &self,
payment_hash: &str, payment_hash: &str,
) -> Result<MintQuoteState, Self::Err> { ) -> Result<MintQuoteState, Self::Err> {
@@ -292,7 +311,7 @@ impl MintLightning for LNbits {
async fn check_outgoing_payment( async fn check_outgoing_payment(
&self, &self,
payment_hash: &str, payment_hash: &str,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let payment = self let payment = self
.lnbits_api .lnbits_api
.get_payment_info(payment_hash) .get_payment_info(payment_hash)
@@ -303,15 +322,15 @@ impl MintLightning for LNbits {
Self::Err::Anyhow(anyhow!("Could not check invoice status")) 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_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), status: lnbits_to_melt_status(&payment.details.status, payment.details.pending),
total_spent: Amount::from( total_spent: Amount::from(
payment.details.amount.unsigned_abs() payment.details.amount.unsigned_abs()
+ payment.details.fee.unsigned_abs() / MSAT_IN_SAT, + payment.details.fee.unsigned_abs() / MSAT_IN_SAT,
), ),
unit: self.get_settings().unit, unit: self.settings.unit.clone(),
}; };
Ok(pay_response) Ok(pay_response)

View File

@@ -18,3 +18,4 @@ tokio.workspace = true
tokio-util.workspace = true tokio-util.workspace = true
tracing.workspace = true tracing.workspace = true
thiserror.workspace = true thiserror.workspace = true
serde_json.workspace = true

View File

@@ -38,7 +38,7 @@ pub enum Error {
InvalidConfig(String), InvalidConfig(String),
} }
impl From<Error> for cdk::cdk_lightning::Error { impl From<Error> for cdk::cdk_payment::Error {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
Self::Lightning(Box::new(e)) Self::Lightning(Box::new(e))
} }

View File

@@ -14,13 +14,14 @@ use std::sync::Arc;
use anyhow::anyhow; use anyhow::anyhow;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
use cdk::cdk_lightning::{ use cdk::cdk_payment::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
PaymentQuoteResponse,
}; };
use cdk::mint::FeeReserve; use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
use cdk::secp256k1::hashes::Hash; use cdk::secp256k1::hashes::Hash;
use cdk::util::{hex, unix_time}; use cdk::types::FeeReserve;
use cdk::util::hex;
use cdk::{mint, Bolt11Invoice}; use cdk::{mint, Bolt11Invoice};
use error::Error; use error::Error;
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit; use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
@@ -45,6 +46,7 @@ pub struct Lnd {
fee_reserve: FeeReserve, fee_reserve: FeeReserve,
wait_invoice_cancel_token: CancellationToken, wait_invoice_cancel_token: CancellationToken,
wait_invoice_is_active: Arc<AtomicBool>, wait_invoice_is_active: Arc<AtomicBool>,
settings: Bolt11Settings,
} }
impl Lnd { impl Lnd {
@@ -96,21 +98,22 @@ impl Lnd {
fee_reserve, fee_reserve,
wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_cancel_token: CancellationToken::new(),
wait_invoice_is_active: Arc::new(AtomicBool::new(false)), wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
settings: Bolt11Settings {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
},
}) })
} }
} }
#[async_trait] #[async_trait]
impl MintLightning for Lnd { impl MintPayment for Lnd {
type Err = cdk_lightning::Error; type Err = cdk_payment::Error;
#[instrument(skip_all)] #[instrument(skip_all)]
fn get_settings(&self) -> Settings { async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
Settings { Ok(serde_json::to_value(&self.settings)?)
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
}
} }
#[instrument(skip_all)] #[instrument(skip_all)]
@@ -124,7 +127,7 @@ impl MintLightning for Lnd {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn wait_any_invoice( async fn wait_any_incoming_payment(
&self, &self,
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> { ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
let mut client = let mut client =
@@ -183,7 +186,7 @@ impl MintLightning for Lnd {
}, // End of stream }, // End of stream
Err(err) => { Err(err) => {
is_active.store(false, Ordering::SeqCst); 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); tracing::error!("{:?}", err);
None None
@@ -199,11 +202,21 @@ impl MintLightning for Lnd {
#[instrument(skip_all)] #[instrument(skip_all)]
async fn get_payment_quote( async fn get_payment_quote(
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, request: &str,
unit: &CurrencyUnit,
options: Option<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
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 = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -216,7 +229,7 @@ impl MintLightning for Lnd {
}; };
Ok(PaymentQuoteResponse { Ok(PaymentQuoteResponse {
request_lookup_id: melt_quote_request.request.payment_hash().to_string(), request_lookup_id: bolt11.payment_hash().to_string(),
amount, amount,
fee: fee.into(), fee: fee.into(),
state: MeltQuoteState::Unpaid, state: MeltQuoteState::Unpaid,
@@ -224,12 +237,12 @@ impl MintLightning for Lnd {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn pay_invoice( async fn make_payment(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>, partial_amount: Option<Amount>,
max_fee: Option<Amount>, max_fee: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let payment_request = melt_quote.request; let payment_request = melt_quote.request;
let bolt11 = Bolt11Invoice::from_str(&payment_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; total_amt = (route.total_amt_msat / 1000) as u64;
} }
Ok(PayInvoiceResponse { Ok(MakePaymentResponse {
payment_lookup_id: hex::encode(payment_hash), payment_lookup_id: hex::encode(payment_hash),
payment_preimage, payment_proof: payment_preimage,
status, status,
total_spent: total_amt.into(), total_spent: total_amt.into(),
unit: CurrencyUnit::Sat, 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_lookup_id: hex::encode(payment_response.payment_hash),
payment_preimage, payment_proof: payment_preimage,
status, status,
total_spent: total_amount.into(), total_spent: total_amount.into(),
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
@@ -405,16 +418,13 @@ impl MintLightning for Lnd {
} }
#[instrument(skip(self, description))] #[instrument(skip(self, description))]
async fn create_invoice( async fn create_incoming_payment_request(
&self, &self,
amount: Amount, amount: Amount,
unit: &CurrencyUnit, unit: &CurrencyUnit,
description: String, description: String,
unix_expiry: u64, unix_expiry: Option<u64>,
) -> Result<CreateInvoiceResponse, Self::Err> { ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let time_now = unix_time();
assert!(unix_expiry > time_now);
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?; let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice { let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice {
@@ -435,15 +445,15 @@ impl MintLightning for Lnd {
let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?; let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
Ok(CreateInvoiceResponse { Ok(CreateIncomingPaymentResponse {
request_lookup_id: bolt11.payment_hash().to_string(), request_lookup_id: bolt11.payment_hash().to_string(),
request: bolt11, request: bolt11.to_string(),
expiry: Some(unix_expiry), expiry: unix_expiry,
}) })
} }
#[instrument(skip(self))] #[instrument(skip(self))]
async fn check_incoming_invoice_status( async fn check_incoming_payment_status(
&self, &self,
request_lookup_id: &str, request_lookup_id: &str,
) -> Result<MintQuoteState, Self::Err> { ) -> Result<MintQuoteState, Self::Err> {
@@ -479,7 +489,7 @@ impl MintLightning for Lnd {
async fn check_outgoing_payment( async fn check_outgoing_payment(
&self, &self,
payment_hash: &str, payment_hash: &str,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<MakePaymentResponse, Self::Err> {
let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest { let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?, payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
no_inflight_updates: true, no_inflight_updates: true,
@@ -498,15 +508,15 @@ impl MintLightning for Lnd {
Err(err) => { Err(err) => {
let err_code = err.code(); let err_code = err.code();
if err_code == Code::NotFound { if err_code == Code::NotFound {
return Ok(PayInvoiceResponse { return Ok(MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(), payment_lookup_id: payment_hash.to_string(),
payment_preimage: None, payment_proof: None,
status: MeltQuoteState::Unknown, status: MeltQuoteState::Unknown,
total_spent: Amount::ZERO, total_spent: Amount::ZERO,
unit: self.get_settings().unit, unit: self.settings.unit.clone(),
}); });
} else { } 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 status = update.status();
let response = match status { let response = match status {
PaymentStatus::Unknown => PayInvoiceResponse { PaymentStatus::Unknown => MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(), payment_lookup_id: payment_hash.to_string(),
payment_preimage: Some(update.payment_preimage), payment_proof: Some(update.payment_preimage),
status: MeltQuoteState::Unknown, status: MeltQuoteState::Unknown,
total_spent: Amount::ZERO, total_spent: Amount::ZERO,
unit: self.get_settings().unit, unit: self.settings.unit.clone(),
}, },
PaymentStatus::InFlight => { PaymentStatus::InFlight => {
// Continue waiting for the next update // Continue waiting for the next update
continue; continue;
} }
PaymentStatus::Succeeded => PayInvoiceResponse { PaymentStatus::Succeeded => MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(), payment_lookup_id: payment_hash.to_string(),
payment_preimage: Some(update.payment_preimage), payment_proof: Some(update.payment_preimage),
status: MeltQuoteState::Paid, status: MeltQuoteState::Paid,
total_spent: Amount::from( total_spent: Amount::from(
(update (update
@@ -541,12 +551,12 @@ impl MintLightning for Lnd {
), ),
unit: CurrencyUnit::Sat, unit: CurrencyUnit::Sat,
}, },
PaymentStatus::Failed => PayInvoiceResponse { PaymentStatus::Failed => MakePaymentResponse {
payment_lookup_id: payment_hash.to_string(), payment_lookup_id: payment_hash.to_string(),
payment_preimage: Some(update.payment_preimage), payment_proof: Some(update.payment_preimage),
status: MeltQuoteState::Failed, status: MeltQuoteState::Failed,
total_spent: Amount::ZERO, total_spent: Amount::ZERO,
unit: self.get_settings().unit, unit: self.settings.unit.clone(),
}, },
}; };

View File

@@ -19,20 +19,16 @@ cdk = { workspace = true, features = [
"mint", "mint",
] } ] }
clap.workspace = true clap.workspace = true
tonic = { version = "0.12.3", features = [ tonic.workspace = true
"channel",
"tls",
"tls-webpki-roots",
] }
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
tokio.workspace = true tokio.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde.workspace = true serde.workspace = true
thiserror.workspace = true thiserror.workspace = true
prost = "0.13.1" prost.workspace = true
home.workspace = true home.workspace = true
[build-dependencies] [build-dependencies]
tonic-build = "0.12" tonic-build.workspace = true

View File

@@ -10,18 +10,19 @@ description = "CDK mint binary"
rust-version = "1.75.0" rust-version = "1.75.0"
[features] [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 # 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"] 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"] cln = ["dep:cdk-cln"]
lnd = ["dep:cdk-lnd"] lnd = ["dep:cdk-lnd"]
lnbits = ["dep:cdk-lnbits"] lnbits = ["dep:cdk-lnbits"]
fakewallet = ["dep:cdk-fake-wallet"] 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] [dependencies]
anyhow.workspace = true anyhow.workspace = true
@@ -42,6 +43,7 @@ cdk-lnd = { workspace = true, optional = true }
cdk-fake-wallet = { workspace = true, optional = true } cdk-fake-wallet = { workspace = true, optional = true }
cdk-axum.workspace = true cdk-axum.workspace = true
cdk-mint-rpc = { workspace = true, optional = true } cdk-mint-rpc = { workspace = true, optional = true }
cdk-payment-processor = { workspace = true, optional = true }
config = { version = "0.13.3", features = ["toml"] } config = { version = "0.13.3", features = ["toml"] }
clap.workspace = true clap.workspace = true
bitcoin.workspace = true bitcoin.workspace = true
@@ -54,7 +56,7 @@ bip39.workspace = true
tower-http = { workspace = true, features = ["compression-full", "decompression-full"] } tower-http = { workspace = true, features = ["compression-full", "decompression-full"] }
tower = "0.5.2" tower = "0.5.2"
lightning-invoice.workspace = true lightning-invoice.workspace = true
home = "0.5.5" home.workspace = true
url.workspace = true url.workspace = true
utoipa = { workspace = true, optional = true } utoipa = { workspace = true, optional = true }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }

View File

@@ -91,3 +91,10 @@ reserve_fee_min = 4
# reserve_fee_min = 1 # reserve_fee_min = 1
# min_delay_time = 1 # min_delay_time = 1
# max_delay_time = 3 # 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"

View File

@@ -1,9 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::{sha256, Hash};
#[cfg(feature = "fakewallet")] use cdk::nuts::{CurrencyUnit, PublicKey};
use cdk::nuts::CurrencyUnit;
use cdk::nuts::PublicKey;
use cdk::Amount; use cdk::Amount;
use cdk_axum::cache; use cdk_axum::cache;
use config::{Config, ConfigError, File}; use config::{Config, ConfigError, File};
@@ -56,6 +54,8 @@ pub enum LnBackend {
FakeWallet, FakeWallet,
#[cfg(feature = "lnd")] #[cfg(feature = "lnd")]
Lnd, Lnd,
#[cfg(feature = "grpc-processor")]
GrpcProcessor,
} }
impl std::str::FromStr for LnBackend { impl std::str::FromStr for LnBackend {
@@ -71,6 +71,8 @@ impl std::str::FromStr for LnBackend {
"fakewallet" => Ok(LnBackend::FakeWallet), "fakewallet" => Ok(LnBackend::FakeWallet),
#[cfg(feature = "lnd")] #[cfg(feature = "lnd")]
"lnd" => Ok(LnBackend::Lnd), "lnd" => Ok(LnBackend::Lnd),
#[cfg(feature = "grpc-processor")]
"grpcprocessor" => Ok(LnBackend::GrpcProcessor),
_ => Err(format!("Unknown Lightning backend: {}", s)), _ => Err(format!("Unknown Lightning backend: {}", s)),
} }
} }
@@ -165,6 +167,14 @@ fn default_max_delay_time() -> u64 {
3 3
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct GrpcProcessor {
pub supported_units: Vec<CurrencyUnit>,
pub addr: String,
pub port: u16,
pub tls_dir: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum DatabaseEngine { pub enum DatabaseEngine {
@@ -206,6 +216,7 @@ pub struct Settings {
pub lnd: Option<Lnd>, pub lnd: Option<Lnd>,
#[cfg(feature = "fakewallet")] #[cfg(feature = "fakewallet")]
pub fake_wallet: Option<FakeWallet>, pub fake_wallet: Option<FakeWallet>,
pub grpc_processor: Option<GrpcProcessor>,
pub database: Database, pub database: Database,
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
pub mint_management_rpc: Option<MintManagementRpc>, pub mint_management_rpc: Option<MintManagementRpc>,
@@ -313,6 +324,13 @@ impl Settings {
settings.fake_wallet.is_some(), settings.fake_wallet.is_some(),
"FakeWallet backend requires a valid config." "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) Ok(settings)

View File

@@ -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::<Result<Vec<CurrencyUnit>, _>>()
{
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
}
}

View File

@@ -18,6 +18,8 @@ impl Ln {
if let Ok(backend_str) = env::var(ENV_LN_BACKEND) { if let Ok(backend_str) = env::var(ENV_LN_BACKEND) {
if let Ok(backend) = backend_str.parse() { if let Ok(backend) = backend_str.parse() {
self.ln_backend = backend; self.ln_backend = backend;
} else {
tracing::warn!("Unknow payment backend set in env var will attempt to use config file. {backend_str}");
} }
} }

View File

@@ -12,6 +12,8 @@ mod mint_info;
mod cln; mod cln;
#[cfg(feature = "fakewallet")] #[cfg(feature = "fakewallet")]
mod fake_wallet; mod fake_wallet;
#[cfg(feature = "grpc-processor")]
mod grpc_processor;
#[cfg(feature = "lnbits")] #[cfg(feature = "lnbits")]
mod lnbits; mod lnbits;
#[cfg(feature = "lnd")] #[cfg(feature = "lnd")]
@@ -28,6 +30,8 @@ pub use cln::*;
pub use common::*; pub use common::*;
#[cfg(feature = "fakewallet")] #[cfg(feature = "fakewallet")]
pub use fake_wallet::*; pub use fake_wallet::*;
#[cfg(feature = "grpc-processor")]
pub use grpc_processor::*;
pub use ln::*; pub use ln::*;
#[cfg(feature = "lnbits")] #[cfg(feature = "lnbits")]
pub use lnbits::*; pub use lnbits::*;
@@ -77,6 +81,11 @@ impl Settings {
LnBackend::Lnd => { LnBackend::Lnd => {
self.lnd = Some(self.lnd.clone().unwrap_or_default().from_env()); 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"), LnBackend::None => bail!("Ln backend must be set"),
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
_ => bail!("Selected Ln backend is not enabled in this build"), _ => bail!("Selected Ln backend is not enabled in this build"),

View File

@@ -19,11 +19,19 @@ use cdk::mint::{MintBuilder, MintMeltLimits};
feature = "cln", feature = "cln",
feature = "lnbits", feature = "lnbits",
feature = "lnd", feature = "lnd",
feature = "fakewallet" feature = "fakewallet",
feature = "grpc-processor"
))] ))]
use cdk::nuts::nut17::SupportedMethods; use cdk::nuts::nut17::SupportedMethods;
use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; 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::types::QuoteTTL;
use cdk_axum::cache::HttpCache; use cdk_axum::cache::HttpCache;
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
@@ -52,10 +60,11 @@ const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION")
feature = "cln", feature = "cln",
feature = "lnbits", feature = "lnbits",
feature = "lnd", feature = "lnd",
feature = "fakewallet" feature = "fakewallet",
feature = "grpc-processor"
)))] )))]
compile_error!( 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] #[tokio::main]
@@ -169,6 +178,8 @@ async fn main() -> anyhow::Result<()> {
melt_max: settings.ln.max_melt, melt_max: settings.ln.max_melt,
}; };
tracing::debug!("Ln backendd: {:?}", settings.ln.ln_backend);
match settings.ln.ln_backend { match settings.ln.ln_backend {
#[cfg(feature = "cln")] #[cfg(feature = "cln")]
LnBackend::Cln => { LnBackend::Cln => {
@@ -182,12 +193,14 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
let cln = Arc::new(cln); let cln = Arc::new(cln);
mint_builder = mint_builder.add_ln_backend( mint_builder = mint_builder
CurrencyUnit::Sat, .add_ln_backend(
PaymentMethod::Bolt11, CurrencyUnit::Sat,
mint_melt_limits, PaymentMethod::Bolt11,
cln.clone(), mint_melt_limits,
); cln.clone(),
)
.await?;
let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); 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) .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
.await?; .await?;
mint_builder = mint_builder.add_ln_backend( mint_builder = mint_builder
CurrencyUnit::Sat, .add_ln_backend(
PaymentMethod::Bolt11, CurrencyUnit::Sat,
mint_melt_limits, PaymentMethod::Bolt11,
Arc::new(lnbits), mint_melt_limits,
); Arc::new(lnbits),
)
.await?;
let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
mint_builder = mint_builder.add_supported_websockets(nut17_supported); 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) .setup(&mut ln_routers, &settings, CurrencyUnit::Msat)
.await?; .await?;
mint_builder = mint_builder.add_ln_backend( mint_builder = mint_builder
CurrencyUnit::Sat, .add_ln_backend(
PaymentMethod::Bolt11, CurrencyUnit::Sat,
mint_melt_limits, PaymentMethod::Bolt11,
Arc::new(lnd), mint_melt_limits,
); Arc::new(lnd),
)
.await?;
let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat); let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, CurrencyUnit::Sat);
@@ -231,27 +249,72 @@ async fn main() -> anyhow::Result<()> {
#[cfg(feature = "fakewallet")] #[cfg(feature = "fakewallet")]
LnBackend::FakeWallet => { LnBackend::FakeWallet => {
let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); 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 { for unit in fake_wallet.clone().supported_units {
let fake = fake_wallet let fake = fake_wallet
.setup(&mut ln_routers, &settings, CurrencyUnit::Sat) .setup(&mut ln_routers, &settings, CurrencyUnit::Sat)
.await?; .await
.expect("hhh");
let fake = Arc::new(fake); let fake = Arc::new(fake);
mint_builder = mint_builder.add_ln_backend( mint_builder = mint_builder
unit.clone(), .add_ln_backend(
PaymentMethod::Bolt11, unit.clone(),
mint_melt_limits, PaymentMethod::Bolt11,
fake.clone(), mint_melt_limits,
); fake.clone(),
)
.await?;
let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit); let nut17_supported = SupportedMethods::new(PaymentMethod::Bolt11, unit);
mint_builder = mint_builder.add_supported_websockets(nut17_supported); 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 { 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?; let mint = mint_builder.build().await?;
tracing::debug!("Mint built from builder.");
let mint = Arc::new(mint); let mint = Arc::new(mint);
// Check the status of any mint quotes that are pending // Check the status of any mint quotes that are pending
@@ -425,6 +490,13 @@ async fn main() -> anyhow::Result<()> {
Ok(()) 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<PathBuf> { fn work_dir() -> Result<PathBuf> {
let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?; let home_dir = home::home_dir().ok_or(anyhow!("Unknown home dir"))?;
let dir = home_dir.join(".cdk-mintd"); let dir = home_dir.join(".cdk-mintd");
@@ -433,10 +505,3 @@ fn work_dir() -> Result<PathBuf> {
Ok(dir) Ok(dir)
} }
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C handler");
tracing::info!("Shutdown signal received");
}

View File

@@ -11,11 +11,17 @@ use async_trait::async_trait;
use axum::Router; use axum::Router;
#[cfg(feature = "fakewallet")] #[cfg(feature = "fakewallet")]
use bip39::rand::{thread_rng, Rng}; use bip39::rand::{thread_rng, Rng};
use cdk::cdk_lightning::MintLightning; use cdk::cdk_payment::MintPayment;
use cdk::mint::FeeReserve;
#[cfg(feature = "lnbits")] #[cfg(feature = "lnbits")]
use cdk::mint_url::MintUrl; use cdk::mint_url::MintUrl;
use cdk::nuts::CurrencyUnit; use cdk::nuts::CurrencyUnit;
#[cfg(any(
feature = "lnbits",
feature = "cln",
feature = "lnd",
feature = "fakewallet"
))]
use cdk::types::FeeReserve;
#[cfg(feature = "lnbits")] #[cfg(feature = "lnbits")]
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -30,7 +36,7 @@ pub trait LnBackendSetup {
routers: &mut Vec<Router>, routers: &mut Vec<Router>,
settings: &Settings, settings: &Settings,
unit: CurrencyUnit, unit: CurrencyUnit,
) -> anyhow::Result<impl MintLightning>; ) -> anyhow::Result<impl MintPayment>;
} }
#[cfg(feature = "cln")] #[cfg(feature = "cln")]
@@ -162,3 +168,23 @@ impl LnBackendSetup for config::FakeWallet {
Ok(fake_wallet) Ok(fake_wallet)
} }
} }
#[cfg(feature = "grpc-processor")]
#[async_trait]
impl LnBackendSetup for config::GrpcProcessor {
async fn setup(
&self,
_routers: &mut Vec<Router>,
_settings: &Settings,
_unit: CurrencyUnit,
) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
&self.addr,
self.port,
self.tls_dir.clone(),
)
.await?;
Ok(payment_processor)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=src/proto/payment_processor.proto");
tonic_build::compile_protos("src/proto/payment_processor.proto")?;
Ok(())
}

View File

@@ -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<PathBuf> = env::var(ENV_PAYMENT_PROCESSOR_TLS_DIR)
.ok()
.map(PathBuf::from);
let ln_backed: Arc<dyn MintPayment<Err = payment::Error> + 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::<u64>() {
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::<u64>() {
self.reserve_fee_min = reserve_fee.into();
}
}
self
}
}

View File

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

View File

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

View File

@@ -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<Channel>,
wait_incoming_payment_stream_is_active: Arc<AtomicBool>,
cancel_incoming_payment_listener: CancellationToken,
}
impl PaymentProcessorClient {
/// Payment Processor
pub async fn new(addr: &str, port: u16, tls_dir: Option<PathBuf>) -> anyhow::Result<Self> {
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<Value, Self::Err> {
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<u64>,
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
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<MeltOptions>,
) -> Result<PaymentQuoteResponse, Self::Err> {
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<Amount>,
max_fee_amount: Option<Amount>,
) -> Result<CdkMakePaymentResponse, Self::Err> {
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<Pin<Box<dyn Stream<Item = String> + 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<MintQuoteState, Self::Err> {
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<CdkMakePaymentResponse, Self::Err> {
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)?)
}
}

View File

@@ -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<MakePaymentResponse> for CdkMakePaymentResponse {
type Error = crate::error::Error;
fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
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<CdkMakePaymentResponse> 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<CreateIncomingPaymentResponse> 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<CreatePaymentResponse> for CreateIncomingPaymentResponse {
type Error = crate::error::Error;
fn try_from(value: CreatePaymentResponse) -> Result<Self, Self::Error> {
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<cdk_common::payment::PaymentQuoteResponse> 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<cdk_common::nut05::MeltOptions> for MeltOptions {
fn from(value: cdk_common::nut05::MeltOptions) -> Self {
Self {
options: Some(value.into()),
}
}
}
impl From<cdk_common::nut05::MeltOptions> 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<MeltOptions> 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<PaymentQuoteResponse> 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<QuoteState> 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<cdk_common::nut05::QuoteState> 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<cdk_common::nut04::QuoteState> 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<cdk_common::mint::MeltQuote> 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<MeltQuote> for cdk_common::mint::MeltQuote {
type Error = crate::error::Error;
fn try_from(value: MeltQuote) -> Result<Self, Self::Error> {
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<PaymentQuoteRequest> for MeltQuoteBolt11Request {
type Error = crate::error::Error;
fn try_from(value: PaymentQuoteRequest) -> Result<Self, Self::Error> {
Ok(Self {
request: Bolt11Invoice::from_str(&value.request)?,
unit: CurrencyUnit::from_str(&value.unit)?,
options: value.options.map(|o| o.into()),
})
}
}

View File

@@ -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;
}

View File

@@ -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<Box<dyn Stream<Item = Result<WaitIncomingPaymentResponse, Status>> + Send>>;
/// Payment Processor
#[derive(Clone)]
pub struct PaymentProcessorServer {
inner: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
socket_addr: SocketAddr,
shutdown: Arc<Notify>,
handle: Option<Arc<JoinHandle<anyhow::Result<()>>>>,
}
impl PaymentProcessorServer {
pub fn new(
payment_processor: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
addr: &str,
port: u16,
) -> anyhow::Result<Self> {
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<PathBuf>) -> 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<SettingsRequest>,
) -> Result<Response<SettingsResponse>, 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<CreatePaymentRequest>,
) -> Result<Response<CreatePaymentResponse>, 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<PaymentQuoteRequest>,
) -> Result<Response<PaymentQuoteResponse>, Status> {
let request = request.into_inner();
let options: Option<cdk_common::MeltOptions> =
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<MakePaymentRequest>,
) -> Result<Response<MakePaymentResponse>, 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<CheckIncomingPaymentRequest>,
) -> Result<Response<CheckIncomingPaymentResponse>, 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<CheckOutgoingPaymentRequest>,
) -> Result<Response<MakePaymentResponse>, 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<WaitIncomingPaymentRequest>,
) -> Result<Response<Self::WaitIncomingPaymentStream>, 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
))
}
}

View File

@@ -7,7 +7,7 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; 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::database::{self, MintDatabase};
use cdk_common::dhke::hash_to_curve; use cdk_common::dhke::hash_to_curve;
use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
@@ -826,7 +826,7 @@ impl MintDatabase for MintRedbDatabase {
async fn add_melt_request( async fn add_melt_request(
&self, &self,
melt_request: MeltBolt11Request<Uuid>, melt_request: MeltBolt11Request<Uuid>,
ln_key: LnKey, ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err> { ) -> Result<(), Self::Err> {
let write_txn = self.db.begin_write().map_err(Error::from)?; let write_txn = self.db.begin_write().map_err(Error::from)?;
let mut table = write_txn.open_table(MELT_REQUESTS).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( async fn get_melt_request(
&self, &self,
quote_id: &Uuid, quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err> { ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
let read_txn = self.db.begin_read().map_err(Error::from)?; let read_txn = self.db.begin_read().map_err(Error::from)?;
let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?; let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;

View File

@@ -1,7 +1,7 @@
//! In-memory database that is provided by the `cdk-sqlite` crate, mainly for testing purposes. //! In-memory database that is provided by the `cdk-sqlite` crate, mainly for testing purposes.
use std::collections::HashMap; use std::collections::HashMap;
use cdk_common::common::LnKey; use cdk_common::common::PaymentProcessorKey;
use cdk_common::database::{self, MintDatabase}; use cdk_common::database::{self, MintDatabase};
use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs}; use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs};
@@ -29,7 +29,7 @@ pub async fn new_with_state(
melt_quotes: Vec<mint::MeltQuote>, melt_quotes: Vec<mint::MeltQuote>,
pending_proofs: Proofs, pending_proofs: Proofs,
spent_proofs: Proofs, spent_proofs: Proofs,
melt_request: Vec<(MeltBolt11Request<Uuid>, LnKey)>, melt_request: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
mint_info: MintInfo, mint_info: MintInfo,
) -> Result<MintSqliteDatabase, database::Error> { ) -> Result<MintSqliteDatabase, database::Error> {
let db = empty().await?; let db = empty().await?;

View File

@@ -6,7 +6,7 @@ use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use bitcoin::bip32::DerivationPath; 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::database::{self, MintDatabase};
use cdk_common::mint::{self, MintKeySetInfo, MintQuote}; use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
use cdk_common::nut00::ProofsMethods; use cdk_common::nut00::ProofsMethods;
@@ -1285,7 +1285,7 @@ WHERE keyset_id=?;
async fn add_melt_request( async fn add_melt_request(
&self, &self,
melt_request: MeltBolt11Request<Uuid>, melt_request: MeltBolt11Request<Uuid>,
ln_key: LnKey, ln_key: PaymentProcessorKey,
) -> Result<(), Self::Err> { ) -> Result<(), Self::Err> {
let mut transaction = self.pool.begin().await.map_err(Error::from)?; 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( async fn get_melt_request(
&self, &self,
quote_id: &Uuid, quote_id: &Uuid,
) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err> { ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
let mut transaction = self.pool.begin().await.map_err(Error::from)?; let mut transaction = self.pool.begin().await.map_err(Error::from)?;
let rec = sqlx::query( let rec = sqlx::query(
@@ -1708,7 +1708,9 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
}) })
} }
fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>, LnKey), Error> { fn sqlite_row_to_melt_request(
row: SqliteRow,
) -> Result<(MeltBolt11Request<Uuid>, PaymentProcessorKey), Error> {
let quote_id: Hyphenated = row.try_get("id").map_err(Error::from)?; 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_inputs: String = row.try_get("inputs").map_err(Error::from)?;
let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?; let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
@@ -1721,7 +1723,7 @@ fn sqlite_row_to_melt_request(row: SqliteRow) -> Result<(MeltBolt11Request<Uuid>
outputs: row_outputs.and_then(|o| serde_json::from_str(&o).ok()), 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)?, unit: CurrencyUnit::from_str(&row_unit)?,
method: PaymentMethod::from_str(&row_method)?, method: PaymentMethod::from_str(&row_method)?,
}; };

View File

@@ -28,7 +28,7 @@ pub use cdk_common::{
}; };
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
#[doc(hidden)] #[doc(hidden)]
pub use cdk_common::{lightning as cdk_lightning, subscription}; pub use cdk_common::{payment as cdk_payment, subscription};
pub mod fees; pub mod fees;

View File

@@ -6,18 +6,20 @@ use std::sync::Arc;
use anyhow::anyhow; use anyhow::anyhow;
use bitcoin::bip32::DerivationPath; use bitcoin::bip32::DerivationPath;
use cdk_common::database::{self, MintDatabase}; use cdk_common::database::{self, MintDatabase};
use cdk_common::error::Error;
use cdk_common::payment::Bolt11Settings;
use super::nut17::SupportedMethods; use super::nut17::SupportedMethods;
use super::nut19::{self, CachedEndpoint}; use super::nut19::{self, CachedEndpoint};
use super::Nuts; use super::Nuts;
use crate::amount::Amount; use crate::amount::Amount;
use crate::cdk_lightning::{self, MintLightning}; use crate::cdk_payment::{self, MintPayment};
use crate::mint::Mint; use crate::mint::Mint;
use crate::nuts::{ use crate::nuts::{
ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion,
MppMethodSettings, PaymentMethod, MppMethodSettings, PaymentMethod,
}; };
use crate::types::LnKey; use crate::types::PaymentProcessorKey;
/// Cashu Mint /// Cashu Mint
#[derive(Default)] #[derive(Default)]
@@ -27,7 +29,9 @@ pub struct MintBuilder {
/// Mint Storage backend /// Mint Storage backend
localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>, localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
/// Ln backends for mint /// Ln backends for mint
ln: Option<HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>>, ln: Option<
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
>,
seed: Option<Vec<u8>>, seed: Option<Vec<u8>>,
supported_units: HashMap<CurrencyUnit, (u64, u8)>, supported_units: HashMap<CurrencyUnit, (u64, u8)>,
custom_paths: HashMap<CurrencyUnit, DerivationPath>, custom_paths: HashMap<CurrencyUnit, DerivationPath>,
@@ -119,25 +123,27 @@ impl MintBuilder {
} }
/// Add ln backend /// Add ln backend
pub fn add_ln_backend( pub async fn add_ln_backend(
mut self, mut self,
unit: CurrencyUnit, unit: CurrencyUnit,
method: PaymentMethod, method: PaymentMethod,
limits: MintMeltLimits, limits: MintMeltLimits,
ln_backend: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>, ln_backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
) -> Self { ) -> Result<Self, Error> {
let ln_key = LnKey { let ln_key = PaymentProcessorKey {
unit: unit.clone(), unit: unit.clone(),
method, method: method.clone(),
}; };
let mut ln = self.ln.unwrap_or_default(); 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 { if settings.mpp {
let mpp_settings = MppMethodSettings { let mpp_settings = MppMethodSettings {
method, method: method.clone(),
unit: unit.clone(), unit: unit.clone(),
}; };
@@ -150,7 +156,7 @@ impl MintBuilder {
if method == PaymentMethod::Bolt11 { if method == PaymentMethod::Bolt11 {
let mint_method_settings = MintMethodSettings { let mint_method_settings = MintMethodSettings {
method, method: method.clone(),
unit: unit.clone(), unit: unit.clone(),
min_amount: Some(limits.mint_min), min_amount: Some(limits.mint_min),
max_amount: Some(limits.mint_max), max_amount: Some(limits.mint_max),
@@ -161,7 +167,7 @@ impl MintBuilder {
self.mint_info.nuts.nut04.disabled = false; self.mint_info.nuts.nut04.disabled = false;
let melt_method_settings = MeltMethodSettings { let melt_method_settings = MeltMethodSettings {
method, method: method.clone(),
unit, unit,
min_amount: Some(limits.melt_min), min_amount: Some(limits.melt_min),
max_amount: Some(limits.melt_max), max_amount: Some(limits.melt_max),
@@ -179,7 +185,7 @@ impl MintBuilder {
self.ln = Some(ln); self.ln = Some(ln);
self Ok(self)
} }
/// Set pubkey /// Set pubkey

View File

@@ -1,4 +1,4 @@
use cdk_common::common::LnKey; use cdk_common::common::PaymentProcessorKey;
use cdk_common::MintQuoteState; use cdk_common::MintQuoteState;
use super::Mint; use super::Mint;
@@ -14,7 +14,7 @@ impl Mint {
.await? .await?
.ok_or(Error::UnknownQuote)?; .ok_or(Error::UnknownQuote)?;
let ln = match self.ln.get(&LnKey::new( let ln = match self.ln.get(&PaymentProcessorKey::new(
quote.unit.clone(), quote.unit.clone(),
cdk_common::PaymentMethod::Bolt11, cdk_common::PaymentMethod::Bolt11,
)) { )) {
@@ -27,7 +27,7 @@ impl Mint {
}; };
let ln_status = ln let ln_status = ln
.check_incoming_invoice_status(&quote.request_lookup_id) .check_incoming_payment_status(&quote.request_lookup_id)
.await?; .await?;
if ln_status != quote.state && quote.state != MintQuoteState::Issued { if ln_status != quote.state && quote.state != MintQuoteState::Issued {

View File

@@ -12,14 +12,14 @@ use super::{
Mint, PaymentMethod, PublicKey, State, Mint, PaymentMethod, PublicKey, State,
}; };
use crate::amount::to_unit; 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::verification::Verification;
use crate::mint::SigFlag; use crate::mint::SigFlag;
use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag}; use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag};
use crate::nuts::MeltQuoteState; use crate::nuts::MeltQuoteState;
use crate::types::LnKey; use crate::types::PaymentProcessorKey;
use crate::util::unix_time; use crate::util::unix_time;
use crate::{cdk_lightning, ensure_cdk, Amount, Error}; use crate::{cdk_payment, ensure_cdk, Amount, Error};
impl Mint { impl Mint {
#[instrument(skip_all)] #[instrument(skip_all)]
@@ -112,22 +112,32 @@ impl Mint {
let ln = self let ln = self
.ln .ln
.get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) .get(&PaymentProcessorKey::new(
unit.clone(),
PaymentMethod::Bolt11,
))
.ok_or_else(|| { .ok_or_else(|| {
tracing::info!("Could not get ln backend for {}, bolt11 ", unit); tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
Error::UnsupportedUnit Error::UnsupportedUnit
})?; })?;
let payment_quote = ln.get_payment_quote(melt_request).await.map_err(|err| { let payment_quote = ln
tracing::error!( .get_payment_quote(
"Could not get payment quote for mint quote, {} bolt11, {}", &melt_request.request.to_string(),
unit, &melt_request.unit,
err 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 // 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 // or we want to ignore the amount and do an mpp payment
@@ -385,9 +395,9 @@ impl Mint {
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> { ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
use std::sync::Arc; use std::sync::Arc;
async fn check_payment_state( async fn check_payment_state(
ln: Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>, ln: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
melt_quote: &MeltQuote, melt_quote: &MeltQuote,
) -> anyhow::Result<PayInvoiceResponse> { ) -> anyhow::Result<MakePaymentResponse> {
match ln match ln
.check_outgoing_payment(&melt_quote.request_lookup_id) .check_outgoing_payment(&melt_quote.request_lookup_id)
.await .await
@@ -464,10 +474,10 @@ impl Mint {
_ => None, _ => None,
}; };
tracing::debug!("partial_amount: {:?}", partial_amount); tracing::debug!("partial_amount: {:?}", partial_amount);
let ln = match self let ln = match self.ln.get(&PaymentProcessorKey::new(
.ln quote.unit.clone(),
.get(&LnKey::new(quote.unit.clone(), PaymentMethod::Bolt11)) PaymentMethod::Bolt11,
{ )) {
Some(ln) => ln, Some(ln) => ln,
None => { None => {
tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit); tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
@@ -480,7 +490,7 @@ impl Mint {
}; };
let pre = match ln 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 .await
{ {
Ok(pay) Ok(pay)
@@ -503,7 +513,7 @@ impl Mint {
Err(err) => { Err(err) => {
// If the error is that the invoice was already paid we do not want to hold // 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. // 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"); tracing::debug!("Invoice already paid, resetting melt quote");
if let Err(err) = self.process_unpaid_melt(melt_request).await { if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!("Could not reset melt quote state: {}", err); 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)
} }
}; };

View File

@@ -1,3 +1,4 @@
use cdk_common::payment::Bolt11Settings;
use tracing::instrument; use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
@@ -7,7 +8,7 @@ use super::{
NotificationPayload, PaymentMethod, PublicKey, NotificationPayload, PaymentMethod, PublicKey,
}; };
use crate::nuts::MintQuoteState; use crate::nuts::MintQuoteState;
use crate::types::LnKey; use crate::types::PaymentProcessorKey;
use crate::util::unix_time; use crate::util::unix_time;
use crate::{ensure_cdk, Amount, Error}; use crate::{ensure_cdk, Amount, Error};
@@ -29,10 +30,10 @@ impl Mint {
let is_above_max = settings let is_above_max = settings
.max_amount .max_amount
.map_or(false, |max_amount| amount > max_amount); .is_some_and(|max_amount| amount > max_amount);
let is_below_min = settings let is_below_min = settings
.min_amount .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; let is_out_of_range = is_above_max || is_below_min;
ensure_cdk!( ensure_cdk!(
@@ -64,7 +65,10 @@ impl Mint {
let ln = self let ln = self
.ln .ln
.get(&LnKey::new(unit.clone(), PaymentMethod::Bolt11)) .get(&PaymentProcessorKey::new(
unit.clone(),
PaymentMethod::Bolt11,
))
.ok_or_else(|| { .ok_or_else(|| {
tracing::info!("Bolt11 mint request for unsupported unit"); tracing::info!("Bolt11 mint request for unsupported unit");
@@ -75,17 +79,20 @@ impl Mint {
let quote_expiry = unix_time() + mint_ttl; 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"); tracing::error!("Backend does not support invoice description");
return Err(Error::InvoiceDescriptionUnsupported); return Err(Error::InvoiceDescriptionUnsupported);
} }
let create_invoice_response = ln let create_invoice_response = ln
.create_invoice( .create_incoming_payment_request(
amount, amount,
&unit, &unit,
description.unwrap_or("".to_string()), description.unwrap_or("".to_string()),
quote_expiry, Some(quote_expiry),
) )
.await .await
.map_err(|err| { .map_err(|err| {

View File

@@ -5,18 +5,17 @@ use std::sync::Arc;
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
use bitcoin::secp256k1::{self, Secp256k1}; 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::database::{self, MintDatabase};
use cdk_common::mint::MintKeySetInfo; use cdk_common::mint::MintKeySetInfo;
use futures::StreamExt; use futures::StreamExt;
use serde::{Deserialize, Serialize};
use subscription::PubSubManager; use subscription::PubSubManager;
use tokio::sync::{Notify, RwLock}; use tokio::sync::{Notify, RwLock};
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tracing::instrument; use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
use crate::cdk_lightning::{self, MintLightning}; use crate::cdk_payment::{self, MintPayment};
use crate::dhke::{sign_message, verify_message}; use crate::dhke::{sign_message, verify_message};
use crate::error::Error; use crate::error::Error;
use crate::fees::calculate_fee; use crate::fees::calculate_fee;
@@ -44,7 +43,8 @@ pub struct Mint {
/// Mint Storage backend /// Mint Storage backend
pub localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>, pub localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
/// Ln backends for mint /// Ln backends for mint
pub ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>, pub ln:
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
/// Subscription manager /// Subscription manager
pub pubsub_manager: Arc<PubSubManager>, pub pubsub_manager: Arc<PubSubManager>,
secp_ctx: Secp256k1<secp256k1::All>, secp_ctx: Secp256k1<secp256k1::All>,
@@ -59,7 +59,10 @@ impl Mint {
pub async fn new( pub async fn new(
seed: &[u8], seed: &[u8],
localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>, localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
ln: HashMap<LnKey, Arc<dyn MintLightning<Err = cdk_lightning::Error> + Send + Sync>>, ln: HashMap<
PaymentProcessorKey,
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
>,
// Hashmap where the key is the unit and value is (input fee ppk, max_order) // Hashmap where the key is the unit and value is (input fee ppk, max_order)
supported_units: HashMap<CurrencyUnit, (u64, u8)>, supported_units: HashMap<CurrencyUnit, (u64, u8)>,
custom_paths: HashMap<CurrencyUnit, DerivationPath>, custom_paths: HashMap<CurrencyUnit, DerivationPath>,
@@ -117,21 +120,25 @@ impl Mint {
} }
/// Get mint info /// Get mint info
#[instrument(skip_all)]
pub async fn mint_info(&self) -> Result<MintInfo, Error> { pub async fn mint_info(&self) -> Result<MintInfo, Error> {
Ok(self.localstore.get_mint_info().await?) Ok(self.localstore.get_mint_info().await?)
} }
/// Set mint info /// Set mint info
#[instrument(skip_all)]
pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> { pub async fn set_mint_info(&self, mint_info: MintInfo) -> Result<(), Error> {
Ok(self.localstore.set_mint_info(mint_info).await?) Ok(self.localstore.set_mint_info(mint_info).await?)
} }
/// Get quote ttl /// Get quote ttl
#[instrument(skip_all)]
pub async fn quote_ttl(&self) -> Result<QuoteTTL, Error> { pub async fn quote_ttl(&self) -> Result<QuoteTTL, Error> {
Ok(self.localstore.get_quote_ttl().await?) Ok(self.localstore.get_quote_ttl().await?)
} }
/// Set quote ttl /// Set quote ttl
#[instrument(skip_all)]
pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> { pub async fn set_quote_ttl(&self, quote_ttl: QuoteTTL) -> Result<(), Error> {
Ok(self.localstore.set_quote_ttl(quote_ttl).await?) Ok(self.localstore.set_quote_ttl(quote_ttl).await?)
} }
@@ -139,6 +146,7 @@ impl Mint {
/// Wait for any invoice to be paid /// Wait for any invoice to be paid
/// For each backend starts a task that waits 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 /// Once invoice is paid mint quote status is updated
#[instrument(skip_all)]
pub async fn wait_for_paid_invoices(&self, shutdown: Arc<Notify>) -> Result<(), Error> { pub async fn wait_for_paid_invoices(&self, shutdown: Arc<Notify>) -> Result<(), Error> {
let mint_arc = Arc::new(self.clone()); let mint_arc = Arc::new(self.clone());
@@ -146,19 +154,21 @@ impl Mint {
for (key, ln) in self.ln.iter() { for (key, ln) in self.ln.iter() {
if !ln.is_wait_invoice_active() { if !ln.is_wait_invoice_active() {
tracing::info!("Wait payment for {:?} inactive starting.", key);
let mint = Arc::clone(&mint_arc); let mint = Arc::clone(&mint_arc);
let ln = Arc::clone(ln); let ln = Arc::clone(ln);
let shutdown = Arc::clone(&shutdown); let shutdown = Arc::clone(&shutdown);
let key = key.clone(); let key = key.clone();
join_set.spawn(async move { join_set.spawn(async move {
loop { loop {
tracing::info!("Restarting wait for: {:?}", key);
tokio::select! { tokio::select! {
_ = shutdown.notified() => { _ = shutdown.notified() => {
tracing::info!("Shutdown signal received, stopping task for {:?}", key); tracing::info!("Shutdown signal received, stopping task for {:?}", key);
ln.cancel_wait_invoice(); ln.cancel_wait_invoice();
break; break;
} }
result = ln.wait_any_invoice() => { result = ln.wait_any_incoming_payment() => {
match result { match result {
Ok(mut stream) => { Ok(mut stream) => {
while let Some(request_lookup_id) = stream.next().await { while let Some(request_lookup_id) = stream.next().await {
@@ -168,7 +178,7 @@ impl Mint {
} }
} }
Err(err) => { 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; 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 /// Generate new [`MintKeySetInfo`] from path
#[instrument(skip_all)] #[instrument(skip_all)]
fn create_new_keyset<C: secp256k1::Signing>( fn create_new_keyset<C: secp256k1::Signing>(
@@ -490,7 +491,7 @@ mod tests {
use std::str::FromStr; use std::str::FromStr;
use bitcoin::Network; use bitcoin::Network;
use cdk_common::common::LnKey; use cdk_common::common::PaymentProcessorKey;
use cdk_sqlite::mint::memory::new_with_state; use cdk_sqlite::mint::memory::new_with_state;
use secp256k1::Secp256k1; use secp256k1::Secp256k1;
use uuid::Uuid; use uuid::Uuid;
@@ -594,7 +595,7 @@ mod tests {
seed: &'a [u8], seed: &'a [u8],
mint_info: MintInfo, mint_info: MintInfo,
supported_units: HashMap<CurrencyUnit, (u64, u8)>, supported_units: HashMap<CurrencyUnit, (u64, u8)>,
melt_requests: Vec<(MeltBolt11Request<Uuid>, LnKey)>, melt_requests: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
} }
async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> { async fn create_mint(config: MintConfig<'_>) -> Result<Mint, Error> {

View File

@@ -5,7 +5,7 @@
use super::{Error, Mint}; use super::{Error, Mint};
use crate::mint::{MeltQuote, MeltQuoteState, PaymentMethod}; use crate::mint::{MeltQuote, MeltQuoteState, PaymentMethod};
use crate::types::LnKey; use crate::types::PaymentProcessorKey;
impl Mint { impl Mint {
/// Check the status of all pending mint quotes in the mint db /// 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 { let (melt_request, ln_key) = match melt_request_ln_key {
None => { None => {
let ln_key = LnKey { let ln_key = PaymentProcessorKey {
unit: pending_quote.unit, unit: pending_quote.unit,
method: PaymentMethod::Bolt11, method: PaymentMethod::Bolt11,
}; };
@@ -67,7 +67,7 @@ impl Mint {
if let Err(err) = self if let Err(err) = self
.process_melt_request( .process_melt_request(
&melt_request, &melt_request,
pay_invoice_response.payment_preimage, pay_invoice_response.payment_proof,
pay_invoice_response.total_spent, pay_invoice_response.total_spent,
) )
.await .await

View File

@@ -209,7 +209,7 @@ impl Wallet {
.ok_or(Error::UnknownKeySet)? .ok_or(Error::UnknownKeySet)?
.input_fee_ppk; .input_fee_ppk;
let fee = (input_fee_ppk * count + 999) / 1000; let fee = (input_fee_ppk * count).div_ceil(1000);
Ok(Amount::from(fee)) Ok(Amount::from(fee))
} }

102
justfile
View File

@@ -1,6 +1,3 @@
import "./misc/justfile.custom.just"
import "./misc/test.just"
alias b := build alias b := build
alias c := check alias c := check
alias t := test alias t := test
@@ -66,3 +63,102 @@ typos:
[no-exit-message] [no-exit-message]
typos-fix: typos-fix:
just typos -w 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

154
misc/mintd_payment_processor.sh Executable file
View File

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

View File

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