mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 23:55:01 +01:00
feat: payment processor
This commit is contained in:
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -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
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@@ -25,6 +25,7 @@ cdk-cln = { path = "./crates/cdk-cln", version = "=0.7.1" }
|
|||||||
cdk-lnbits = { path = "./crates/cdk-lnbits", version = "=0.7.1" }
|
cdk-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]
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
&[
|
&[
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
176
crates/cdk-integration-tests/tests/payment_processor.rs
Normal file
176
crates/cdk-integration-tests/tests/payment_processor.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
44
crates/cdk-mintd/src/env_vars/grpc_processor.rs
Normal file
44
crates/cdk-mintd/src/env_vars/grpc_processor.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
65
crates/cdk-payment-processor/Cargo.toml
Normal file
65
crates/cdk-payment-processor/Cargo.toml
Normal 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
|
||||||
77
crates/cdk-payment-processor/README.md
Normal file
77
crates/cdk-payment-processor/README.md
Normal 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.
|
||||||
5
crates/cdk-payment-processor/build.rs
Normal file
5
crates/cdk-payment-processor/build.rs
Normal 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(())
|
||||||
|
}
|
||||||
206
crates/cdk-payment-processor/src/bin/payment_processor.rs
Normal file
206
crates/cdk-payment-processor/src/bin/payment_processor.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
20
crates/cdk-payment-processor/src/error.rs
Normal file
20
crates/cdk-payment-processor/src/error.rs
Normal 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),
|
||||||
|
}
|
||||||
8
crates/cdk-payment-processor/src/lib.rs
Normal file
8
crates/cdk-payment-processor/src/lib.rs
Normal 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;
|
||||||
299
crates/cdk-payment-processor/src/proto/client.rs
Normal file
299
crates/cdk-payment-processor/src/proto/client.rs
Normal 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)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
207
crates/cdk-payment-processor/src/proto/mod.rs
Normal file
207
crates/cdk-payment-processor/src/proto/mod.rs
Normal 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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
113
crates/cdk-payment-processor/src/proto/payment_processor.proto
Normal file
113
crates/cdk-payment-processor/src/proto/payment_processor.proto
Normal 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;
|
||||||
|
}
|
||||||
345
crates/cdk-payment-processor/src/proto/server.rs
Normal file
345
crates/cdk-payment-processor/src/proto/server.rs
Normal 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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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)?,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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("e.request_lookup_id)
|
.check_incoming_payment_status("e.request_lookup_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if ln_status != quote.state && quote.state != MintQuoteState::Issued {
|
if ln_status != quote.state && quote.state != MintQuoteState::Issued {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
102
justfile
@@ -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
154
misc/mintd_payment_processor.sh
Executable 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
|
||||||
@@ -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}}"
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user