mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 13:44:55 +01:00
feat: bolt12
This commit is contained in:
@@ -61,6 +61,7 @@ ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
|
|||||||
cbor-diag = "0.1.12"
|
cbor-diag = "0.1.12"
|
||||||
futures = { version = "0.3.28", default-features = false, features = ["async-await"] }
|
futures = { version = "0.3.28", default-features = false, features = ["async-await"] }
|
||||||
lightning-invoice = { version = "0.33.0", features = ["serde", "std"] }
|
lightning-invoice = { version = "0.33.0", features = ["serde", "std"] }
|
||||||
|
lightning = { version = "0.1.2", default-features = false, features = ["std"]}
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
thiserror = { version = "2" }
|
thiserror = { version = "2" }
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ ciborium.workspace = true
|
|||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
lightning-invoice.workspace = true
|
lightning-invoice.workspace = true
|
||||||
|
lightning.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::cmp::Ordering;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -26,6 +27,12 @@ pub enum Error {
|
|||||||
/// Invalid amount
|
/// Invalid amount
|
||||||
#[error("Invalid Amount: {0}")]
|
#[error("Invalid Amount: {0}")]
|
||||||
InvalidAmount(String),
|
InvalidAmount(String),
|
||||||
|
/// Amount undefined
|
||||||
|
#[error("Amount undefined")]
|
||||||
|
AmountUndefined,
|
||||||
|
/// Utf8 parse error
|
||||||
|
#[error(transparent)]
|
||||||
|
Utf8ParseError(#[from] std::string::FromUtf8Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Amount can be any unit
|
/// Amount can be any unit
|
||||||
@@ -181,6 +188,24 @@ impl Amount {
|
|||||||
) -> Result<Amount, Error> {
|
) -> Result<Amount, Error> {
|
||||||
to_unit(self.0, current_unit, target_unit)
|
to_unit(self.0, current_unit, target_unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert to i64
|
||||||
|
pub fn to_i64(self) -> Option<i64> {
|
||||||
|
if self.0 <= i64::MAX as u64 {
|
||||||
|
Some(self.0 as i64)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from i64, returning None if negative
|
||||||
|
pub fn from_i64(value: i64) -> Option<Self> {
|
||||||
|
if value >= 0 {
|
||||||
|
Some(Amount(value as u64))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Amount {
|
impl Default for Amount {
|
||||||
@@ -273,6 +298,27 @@ impl std::ops::Div for Amount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert offer to amount in unit
|
||||||
|
pub fn amount_for_offer(offer: &Offer, unit: &CurrencyUnit) -> Result<Amount, Error> {
|
||||||
|
let offer_amount = offer.amount().ok_or(Error::AmountUndefined)?;
|
||||||
|
|
||||||
|
let (amount, currency) = match offer_amount {
|
||||||
|
lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
|
||||||
|
(amount_msats, CurrencyUnit::Msat)
|
||||||
|
}
|
||||||
|
lightning::offers::offer::Amount::Currency {
|
||||||
|
iso4217_code,
|
||||||
|
amount,
|
||||||
|
} => (
|
||||||
|
amount,
|
||||||
|
CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)
|
||||||
|
.map_err(|_| Error::CannotConvertUnits)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
to_unit(amount, ¤cy, unit).map_err(|_err| Error::CannotConvertUnits)
|
||||||
|
}
|
||||||
|
|
||||||
/// Kinds of targeting that are supported
|
/// Kinds of targeting that are supported
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
|
||||||
pub enum SplitTarget {
|
pub enum SplitTarget {
|
||||||
|
|||||||
@@ -149,6 +149,18 @@ pub enum RoutePath {
|
|||||||
/// Mint Blind Auth
|
/// Mint Blind Auth
|
||||||
#[serde(rename = "/v1/auth/blind/mint")]
|
#[serde(rename = "/v1/auth/blind/mint")]
|
||||||
MintBlindAuth,
|
MintBlindAuth,
|
||||||
|
/// Bolt12 Mint Quote
|
||||||
|
#[serde(rename = "/v1/mint/quote/bolt12")]
|
||||||
|
MintQuoteBolt12,
|
||||||
|
/// Bolt12 Mint
|
||||||
|
#[serde(rename = "/v1/mint/bolt12")]
|
||||||
|
MintBolt12,
|
||||||
|
/// Bolt12 Melt Quote
|
||||||
|
#[serde(rename = "/v1/melt/quote/bolt12")]
|
||||||
|
MeltQuoteBolt12,
|
||||||
|
/// Bolt12 Quote
|
||||||
|
#[serde(rename = "/v1/melt/bolt12")]
|
||||||
|
MeltBolt12,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns [`RoutePath`]s that match regex
|
/// Returns [`RoutePath`]s that match regex
|
||||||
@@ -195,6 +207,8 @@ mod tests {
|
|||||||
assert!(paths.contains(&RoutePath::Checkstate));
|
assert!(paths.contains(&RoutePath::Checkstate));
|
||||||
assert!(paths.contains(&RoutePath::Restore));
|
assert!(paths.contains(&RoutePath::Restore));
|
||||||
assert!(paths.contains(&RoutePath::MintBlindAuth));
|
assert!(paths.contains(&RoutePath::MintBlindAuth));
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
|
||||||
|
assert!(paths.contains(&RoutePath::MintBolt12));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -203,13 +217,17 @@ mod tests {
|
|||||||
let paths = matching_route_paths("^/v1/mint/.*").unwrap();
|
let paths = matching_route_paths("^/v1/mint/.*").unwrap();
|
||||||
|
|
||||||
// Should match only mint paths
|
// Should match only mint paths
|
||||||
assert_eq!(paths.len(), 2);
|
assert_eq!(paths.len(), 4);
|
||||||
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
assert!(paths.contains(&RoutePath::MintBolt11));
|
assert!(paths.contains(&RoutePath::MintBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
|
||||||
|
assert!(paths.contains(&RoutePath::MintBolt12));
|
||||||
|
|
||||||
// Should not match other paths
|
// Should not match other paths
|
||||||
assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
|
assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
|
||||||
assert!(!paths.contains(&RoutePath::MeltBolt11));
|
assert!(!paths.contains(&RoutePath::MeltBolt11));
|
||||||
|
assert!(!paths.contains(&RoutePath::MeltQuoteBolt12));
|
||||||
|
assert!(!paths.contains(&RoutePath::MeltBolt12));
|
||||||
assert!(!paths.contains(&RoutePath::Swap));
|
assert!(!paths.contains(&RoutePath::Swap));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,9 +237,11 @@ mod tests {
|
|||||||
let paths = matching_route_paths(".*/quote/.*").unwrap();
|
let paths = matching_route_paths(".*/quote/.*").unwrap();
|
||||||
|
|
||||||
// Should match only quote paths
|
// Should match only quote paths
|
||||||
assert_eq!(paths.len(), 2);
|
assert_eq!(paths.len(), 4);
|
||||||
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
|
assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt12));
|
||||||
|
assert!(paths.contains(&RoutePath::MeltQuoteBolt12));
|
||||||
|
|
||||||
// Should not match non-quote paths
|
// Should not match non-quote paths
|
||||||
assert!(!paths.contains(&RoutePath::MintBolt11));
|
assert!(!paths.contains(&RoutePath::MintBolt11));
|
||||||
@@ -336,12 +356,14 @@ mod tests {
|
|||||||
"https://example.com/.well-known/openid-configuration"
|
"https://example.com/.well-known/openid-configuration"
|
||||||
);
|
);
|
||||||
assert_eq!(settings.client_id, "client123");
|
assert_eq!(settings.client_id, "client123");
|
||||||
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
|
assert_eq!(settings.protected_endpoints.len(), 5); // 3 mint paths + 1 swap path
|
||||||
|
|
||||||
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
||||||
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
||||||
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let deserlized_protected = settings.protected_endpoints.into_iter().collect();
|
let deserlized_protected = settings.protected_endpoints.into_iter().collect();
|
||||||
|
|||||||
@@ -330,12 +330,14 @@ mod tests {
|
|||||||
let settings: Settings = serde_json::from_str(json).unwrap();
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
assert_eq!(settings.bat_max_mint, 5);
|
assert_eq!(settings.bat_max_mint, 5);
|
||||||
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
|
assert_eq!(settings.protected_endpoints.len(), 5); // 4 mint paths + 1 swap path
|
||||||
|
|
||||||
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
||||||
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
||||||
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt12),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let deserialized_protected = settings.protected_endpoints.into_iter().collect();
|
let deserialized_protected = settings.protected_endpoints.into_iter().collect();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub mod nut18;
|
|||||||
pub mod nut19;
|
pub mod nut19;
|
||||||
pub mod nut20;
|
pub mod nut20;
|
||||||
pub mod nut23;
|
pub mod nut23;
|
||||||
|
pub mod nut24;
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
mod auth;
|
mod auth;
|
||||||
@@ -67,3 +68,4 @@ pub use nut23::{
|
|||||||
MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
|
MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
|
||||||
MintQuoteBolt11Response, QuoteState as MintQuoteState,
|
MintQuoteBolt11Response, QuoteState as MintQuoteState,
|
||||||
};
|
};
|
||||||
|
pub use nut24::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||||
|
|||||||
@@ -641,13 +641,14 @@ impl<'de> Deserialize<'de> for CurrencyUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Payment Method
|
/// Payment Method
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Debug, Clone, 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,
|
||||||
|
/// Bolt12
|
||||||
|
Bolt12,
|
||||||
/// Custom
|
/// Custom
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
@@ -657,6 +658,7 @@ impl FromStr for PaymentMethod {
|
|||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
match value.to_lowercase().as_str() {
|
match value.to_lowercase().as_str() {
|
||||||
"bolt11" => Ok(Self::Bolt11),
|
"bolt11" => Ok(Self::Bolt11),
|
||||||
|
"bolt12" => Ok(Self::Bolt12),
|
||||||
c => Ok(Self::Custom(c.to_string())),
|
c => Ok(Self::Custom(c.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -666,6 +668,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::Bolt12 => write!(f, "bolt12"),
|
||||||
PaymentMethod::Custom(p) => write!(f, "{p}"),
|
PaymentMethod::Custom(p) => write!(f, "{p}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,16 @@ impl Settings {
|
|||||||
.position(|settings| &settings.method == method && &settings.unit == unit)
|
.position(|settings| &settings.method == method && &settings.unit == unit)
|
||||||
.map(|index| self.methods.remove(index))
|
.map(|index| self.methods.remove(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Supported nut04 methods
|
||||||
|
pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
|
||||||
|
self.methods.iter().map(|a| &a.method).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported nut04 units
|
||||||
|
pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
|
||||||
|
self.methods.iter().map(|s| &s.unit).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
|
|||||||
|
|
||||||
// Basic implementation without trait bounds
|
// Basic implementation without trait bounds
|
||||||
impl<Q> MeltRequest<Q> {
|
impl<Q> MeltRequest<Q> {
|
||||||
|
/// Quote Id
|
||||||
|
pub fn quote_id(&self) -> &Q {
|
||||||
|
&self.quote
|
||||||
|
}
|
||||||
|
|
||||||
/// Get inputs (proofs)
|
/// Get inputs (proofs)
|
||||||
pub fn inputs(&self) -> &Proofs {
|
pub fn inputs(&self) -> &Proofs {
|
||||||
&self.inputs
|
&self.inputs
|
||||||
@@ -132,7 +137,7 @@ impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Total [`Amount`] of [`Proofs`]
|
/// Total [`Amount`] of [`Proofs`]
|
||||||
pub fn proofs_amount(&self) -> Result<Amount, Error> {
|
pub fn inputs_amount(&self) -> Result<Amount, Error> {
|
||||||
Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
|
Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
|
||||||
.map_err(|_| Error::AmountOverflow)
|
.map_err(|_| Error::AmountOverflow)
|
||||||
}
|
}
|
||||||
@@ -355,6 +360,18 @@ pub struct Settings {
|
|||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// Supported nut05 methods
|
||||||
|
pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
|
||||||
|
self.methods.iter().map(|a| &a.method).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported nut05 units
|
||||||
|
pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
|
||||||
|
self.methods.iter().map(|s| &s.unit).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::{from_str, json, to_string};
|
use serde_json::{from_str, json, to_string};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use super::PublicKey;
|
|||||||
use crate::nuts::{
|
use crate::nuts::{
|
||||||
CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
|
CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
|
||||||
};
|
};
|
||||||
|
use crate::MintQuoteBolt12Response;
|
||||||
|
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
@@ -69,6 +70,21 @@ impl SupportedMethods {
|
|||||||
commands,
|
commands,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create [`SupportedMethods`] for Bolt12 with all supported commands
|
||||||
|
pub fn default_bolt12(unit: CurrencyUnit) -> Self {
|
||||||
|
let commands = vec![
|
||||||
|
WsCommand::Bolt12MintQuote,
|
||||||
|
WsCommand::Bolt12MeltQuote,
|
||||||
|
WsCommand::ProofState,
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
method: PaymentMethod::Bolt12,
|
||||||
|
unit,
|
||||||
|
commands,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket commands supported by the Cashu mint
|
/// WebSocket commands supported by the Cashu mint
|
||||||
@@ -82,11 +98,23 @@ pub enum WsCommand {
|
|||||||
/// Command to request a Lightning payment for melting tokens
|
/// Command to request a Lightning payment for melting tokens
|
||||||
#[serde(rename = "bolt11_melt_quote")]
|
#[serde(rename = "bolt11_melt_quote")]
|
||||||
Bolt11MeltQuote,
|
Bolt11MeltQuote,
|
||||||
|
/// Websocket support for Bolt12 Mint Quote
|
||||||
|
#[serde(rename = "bolt12_mint_quote")]
|
||||||
|
Bolt12MintQuote,
|
||||||
|
/// Websocket support for Bolt12 Melt Quote
|
||||||
|
#[serde(rename = "bolt12_melt_quote")]
|
||||||
|
Bolt12MeltQuote,
|
||||||
/// Command to check the state of a proof
|
/// Command to check the state of a proof
|
||||||
#[serde(rename = "proof_state")]
|
#[serde(rename = "proof_state")]
|
||||||
ProofState,
|
ProofState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> From<MintQuoteBolt12Response<T>> for NotificationPayload<T> {
|
||||||
|
fn from(mint_quote: MintQuoteBolt12Response<T>) -> NotificationPayload<T> {
|
||||||
|
NotificationPayload::MintQuoteBolt12Response(mint_quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(bound = "T: Serialize + DeserializeOwned")]
|
#[serde(bound = "T: Serialize + DeserializeOwned")]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@@ -98,6 +126,8 @@ pub enum NotificationPayload<T> {
|
|||||||
MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
|
MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
|
||||||
/// Mint Quote Bolt11 Response
|
/// Mint Quote Bolt11 Response
|
||||||
MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
|
MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
|
||||||
|
/// Mint Quote Bolt12 Response
|
||||||
|
MintQuoteBolt12Response(MintQuoteBolt12Response<T>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<ProofState> for NotificationPayload<T> {
|
impl<T> From<ProofState> for NotificationPayload<T> {
|
||||||
@@ -128,6 +158,10 @@ pub enum Notification {
|
|||||||
MeltQuoteBolt11(Uuid),
|
MeltQuoteBolt11(Uuid),
|
||||||
/// MintQuote id is an Uuid
|
/// MintQuote id is an Uuid
|
||||||
MintQuoteBolt11(Uuid),
|
MintQuoteBolt11(Uuid),
|
||||||
|
/// MintQuote id is an Uuid
|
||||||
|
MintQuoteBolt12(Uuid),
|
||||||
|
/// MintQuote id is an Uuid
|
||||||
|
MeltQuoteBolt12(Uuid),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kind
|
/// Kind
|
||||||
|
|||||||
@@ -55,4 +55,10 @@ pub enum Path {
|
|||||||
/// Swap
|
/// Swap
|
||||||
#[serde(rename = "/v1/swap")]
|
#[serde(rename = "/v1/swap")]
|
||||||
Swap,
|
Swap,
|
||||||
|
/// Bolt12 Mint
|
||||||
|
#[serde(rename = "/v1/mint/bolt12")]
|
||||||
|
MintBolt12,
|
||||||
|
/// Bolt12 Melt
|
||||||
|
#[serde(rename = "/v1/melt/bolt12")]
|
||||||
|
MeltBolt12,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ pub enum QuoteState {
|
|||||||
Unpaid,
|
Unpaid,
|
||||||
/// Quote has been paid and wallet can mint
|
/// Quote has been paid and wallet can mint
|
||||||
Paid,
|
Paid,
|
||||||
/// Minting is in progress
|
|
||||||
/// **Note:** This state is to be used internally but is not part of the
|
|
||||||
/// nut.
|
|
||||||
Pending,
|
|
||||||
/// ecash issued for quote
|
/// ecash issued for quote
|
||||||
Issued,
|
Issued,
|
||||||
}
|
}
|
||||||
@@ -67,7 +63,6 @@ impl fmt::Display for QuoteState {
|
|||||||
match self {
|
match self {
|
||||||
Self::Unpaid => write!(f, "UNPAID"),
|
Self::Unpaid => write!(f, "UNPAID"),
|
||||||
Self::Paid => write!(f, "PAID"),
|
Self::Paid => write!(f, "PAID"),
|
||||||
Self::Pending => write!(f, "PENDING"),
|
|
||||||
Self::Issued => write!(f, "ISSUED"),
|
Self::Issued => write!(f, "ISSUED"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +73,6 @@ impl FromStr for QuoteState {
|
|||||||
|
|
||||||
fn from_str(state: &str) -> Result<Self, Self::Err> {
|
fn from_str(state: &str) -> Result<Self, Self::Err> {
|
||||||
match state {
|
match state {
|
||||||
"PENDING" => Ok(Self::Pending),
|
|
||||||
"PAID" => Ok(Self::Paid),
|
"PAID" => Ok(Self::Paid),
|
||||||
"UNPAID" => Ok(Self::Unpaid),
|
"UNPAID" => Ok(Self::Unpaid),
|
||||||
"ISSUED" => Ok(Self::Issued),
|
"ISSUED" => Ok(Self::Issued),
|
||||||
|
|||||||
104
crates/cashu/src/nuts/nut24.rs
Normal file
104
crates/cashu/src/nuts/nut24.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
//! Bolt12
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
#[cfg(feature = "mint")]
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::{CurrencyUnit, MeltOptions, PublicKey};
|
||||||
|
use crate::Amount;
|
||||||
|
|
||||||
|
/// NUT18 Error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Unknown Quote State
|
||||||
|
#[error("Unknown quote state")]
|
||||||
|
UnknownState,
|
||||||
|
/// Amount overflow
|
||||||
|
#[error("Amount Overflow")]
|
||||||
|
AmountOverflow,
|
||||||
|
/// Publickey not defined
|
||||||
|
#[error("Publickey not defined")]
|
||||||
|
PublickeyUndefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint quote request [NUT-24]
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub struct MintQuoteBolt12Request {
|
||||||
|
/// Amount
|
||||||
|
pub amount: Option<Amount>,
|
||||||
|
/// Unit wallet would like to pay with
|
||||||
|
pub unit: CurrencyUnit,
|
||||||
|
/// Memo to create the invoice with
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Pubkey
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint quote response [NUT-24]
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
|
||||||
|
pub struct MintQuoteBolt12Response<Q> {
|
||||||
|
/// Quote Id
|
||||||
|
pub quote: Q,
|
||||||
|
/// Payment request to fulfil
|
||||||
|
pub request: String,
|
||||||
|
/// Amount
|
||||||
|
pub amount: Option<Amount>,
|
||||||
|
/// Unit wallet would like to pay with
|
||||||
|
pub unit: CurrencyUnit,
|
||||||
|
/// Unix timestamp until the quote is valid
|
||||||
|
pub expiry: Option<u64>,
|
||||||
|
/// Pubkey
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
/// Amount that has been paid
|
||||||
|
pub amount_paid: Amount,
|
||||||
|
/// Amount that has been issued
|
||||||
|
pub amount_issued: Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mint")]
|
||||||
|
impl<Q: ToString> MintQuoteBolt12Response<Q> {
|
||||||
|
/// Convert the MintQuote with a quote type Q to a String
|
||||||
|
pub fn to_string_id(&self) -> MintQuoteBolt12Response<String> {
|
||||||
|
MintQuoteBolt12Response {
|
||||||
|
quote: self.quote.to_string(),
|
||||||
|
request: self.request.clone(),
|
||||||
|
amount: self.amount,
|
||||||
|
unit: self.unit.clone(),
|
||||||
|
expiry: self.expiry,
|
||||||
|
pubkey: self.pubkey,
|
||||||
|
amount_paid: self.amount_paid,
|
||||||
|
amount_issued: self.amount_issued,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mint")]
|
||||||
|
impl From<MintQuoteBolt12Response<Uuid>> for MintQuoteBolt12Response<String> {
|
||||||
|
fn from(value: MintQuoteBolt12Response<Uuid>) -> Self {
|
||||||
|
Self {
|
||||||
|
quote: value.quote.to_string(),
|
||||||
|
request: value.request,
|
||||||
|
expiry: value.expiry,
|
||||||
|
amount_paid: value.amount_paid,
|
||||||
|
amount_issued: value.amount_issued,
|
||||||
|
pubkey: value.pubkey,
|
||||||
|
amount: value.amount,
|
||||||
|
unit: value.unit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt quote request [NUT-18]
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub struct MeltQuoteBolt12Request {
|
||||||
|
/// Bolt12 invoice to be paid
|
||||||
|
pub request: String,
|
||||||
|
/// Unit wallet would like to pay with
|
||||||
|
pub unit: CurrencyUnit,
|
||||||
|
/// Payment Options
|
||||||
|
pub options: Option<MeltOptions>,
|
||||||
|
}
|
||||||
213
crates/cdk-axum/src/bolt12_router.rs
Normal file
213
crates/cdk-axum/src/bolt12_router.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use axum::extract::{Json, Path, State};
|
||||||
|
use axum::response::Response;
|
||||||
|
#[cfg(feature = "swagger")]
|
||||||
|
use cdk::error::ErrorResponse;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
|
||||||
|
use cdk::nuts::{
|
||||||
|
MeltQuoteBolt11Response, MeltQuoteBolt12Request, MeltRequest, MintQuoteBolt12Request,
|
||||||
|
MintQuoteBolt12Response, MintRequest, MintResponse,
|
||||||
|
};
|
||||||
|
use paste::paste;
|
||||||
|
use tracing::instrument;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::auth::AuthHeader;
|
||||||
|
use crate::{into_response, post_cache_wrapper, MintState};
|
||||||
|
|
||||||
|
post_cache_wrapper!(post_mint_bolt12, MintRequest<Uuid>, MintResponse);
|
||||||
|
post_cache_wrapper!(
|
||||||
|
post_melt_bolt12,
|
||||||
|
MeltRequest<Uuid>,
|
||||||
|
MeltQuoteBolt11Response<Uuid>
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
|
get,
|
||||||
|
context_path = "/v1",
|
||||||
|
path = "/mint/quote/bolt12",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json")
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
/// Get mint bolt12 quote
|
||||||
|
#[instrument(skip_all, fields(amount = ?payload.amount))]
|
||||||
|
pub async fn post_mint_bolt12_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
|
State(state): State<MintState>,
|
||||||
|
Json(payload): Json<MintQuoteBolt12Request>,
|
||||||
|
) -> Result<Json<MintQuoteBolt12Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt12),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote = state
|
||||||
|
.mint
|
||||||
|
.get_mint_quote(payload.into())
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
|
||||||
|
Ok(Json(quote.try_into().map_err(into_response)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
|
get,
|
||||||
|
context_path = "/v1",
|
||||||
|
path = "/mint/quote/bolt12/{quote_id}",
|
||||||
|
params(
|
||||||
|
("quote_id" = String, description = "The quote ID"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful response", body = MintQuoteBolt12Response<String>, content_type = "application/json"),
|
||||||
|
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
/// Get mint bolt12 quote
|
||||||
|
#[instrument(skip_all, fields(quote_id = ?quote_id))]
|
||||||
|
pub async fn get_check_mint_bolt12_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
|
State(state): State<MintState>,
|
||||||
|
Path(quote_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<MintQuoteBolt12Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt12),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote = state
|
||||||
|
.mint
|
||||||
|
.check_mint_quote("e_id)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
|
||||||
|
Ok(Json(quote.try_into().map_err(into_response)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
|
post,
|
||||||
|
context_path = "/v1",
|
||||||
|
path = "/mint/bolt12",
|
||||||
|
request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
|
||||||
|
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
/// Request a quote for melting tokens
|
||||||
|
#[instrument(skip_all, fields(quote_id = ?payload.quote))]
|
||||||
|
pub async fn post_mint_bolt12(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
|
State(state): State<MintState>,
|
||||||
|
Json(payload): Json<MintRequest<Uuid>>,
|
||||||
|
) -> Result<Json<MintResponse>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt12),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = state
|
||||||
|
.mint
|
||||||
|
.process_mint_request(payload)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Could not process mint: {}", err);
|
||||||
|
into_response(err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
|
post,
|
||||||
|
context_path = "/v1",
|
||||||
|
path = "/melt/quote/bolt12",
|
||||||
|
request_body(content = MeltQuoteBolt12Request, description = "Quote params", content_type = "application/json"),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
|
||||||
|
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
pub async fn post_melt_bolt12_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
|
State(state): State<MintState>,
|
||||||
|
Json(payload): Json<MeltQuoteBolt12Request>,
|
||||||
|
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt12),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote = state
|
||||||
|
.mint
|
||||||
|
.get_melt_quote(payload.into())
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
|
||||||
|
Ok(Json(quote))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
|
post,
|
||||||
|
context_path = "/v1",
|
||||||
|
path = "/melt/bolt12",
|
||||||
|
request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
|
||||||
|
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
|
||||||
|
///
|
||||||
|
/// Requests tokens to be destroyed and sent out via Lightning.
|
||||||
|
pub async fn post_melt_bolt12(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
|
State(state): State<MintState>,
|
||||||
|
Json(payload): Json<MeltRequest<Uuid>>,
|
||||||
|
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt12),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = state.mint.melt(&payload).await.map_err(into_response)?;
|
||||||
|
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ use router_handlers::*;
|
|||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod bolt12_router;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
mod router_handlers;
|
mod router_handlers;
|
||||||
mod ws;
|
mod ws;
|
||||||
@@ -52,6 +53,11 @@ mod swagger_imports {
|
|||||||
#[cfg(feature = "swagger")]
|
#[cfg(feature = "swagger")]
|
||||||
use swagger_imports::*;
|
use swagger_imports::*;
|
||||||
|
|
||||||
|
use crate::bolt12_router::{
|
||||||
|
cache_post_melt_bolt12, cache_post_mint_bolt12, get_check_mint_bolt12_quote,
|
||||||
|
post_melt_bolt12_quote, post_mint_bolt12_quote,
|
||||||
|
};
|
||||||
|
|
||||||
/// CDK Mint State
|
/// CDK Mint State
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MintState {
|
pub struct MintState {
|
||||||
@@ -134,8 +140,8 @@ pub struct MintState {
|
|||||||
pub struct ApiDocV1;
|
pub struct ApiDocV1;
|
||||||
|
|
||||||
/// Create mint [`Router`] with required endpoints for cashu mint with the default cache
|
/// Create mint [`Router`] with required endpoints for cashu mint with the default cache
|
||||||
pub async fn create_mint_router(mint: Arc<Mint>) -> Result<Router> {
|
pub async fn create_mint_router(mint: Arc<Mint>, include_bolt12: bool) -> Result<Router> {
|
||||||
create_mint_router_with_custom_cache(mint, Default::default()).await
|
create_mint_router_with_custom_cache(mint, Default::default(), include_bolt12).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cors_middleware(
|
async fn cors_middleware(
|
||||||
@@ -187,6 +193,7 @@ async fn cors_middleware(
|
|||||||
pub async fn create_mint_router_with_custom_cache(
|
pub async fn create_mint_router_with_custom_cache(
|
||||||
mint: Arc<Mint>,
|
mint: Arc<Mint>,
|
||||||
cache: HttpCache,
|
cache: HttpCache,
|
||||||
|
include_bolt12: bool,
|
||||||
) -> Result<Router> {
|
) -> Result<Router> {
|
||||||
let state = MintState {
|
let state = MintState {
|
||||||
mint,
|
mint,
|
||||||
@@ -223,9 +230,34 @@ pub async fn create_mint_router_with_custom_cache(
|
|||||||
mint_router.nest("/v1", auth_router)
|
mint_router.nest("/v1", auth_router)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mint_router = mint_router.layer(from_fn(cors_middleware));
|
// Conditionally create and merge bolt12_router
|
||||||
|
let mint_router = if include_bolt12 {
|
||||||
|
let bolt12_router = create_bolt12_router(state.clone());
|
||||||
|
mint_router.nest("/v1", bolt12_router)
|
||||||
|
} else {
|
||||||
|
mint_router
|
||||||
|
};
|
||||||
|
|
||||||
let mint_router = mint_router.with_state(state);
|
let mint_router = mint_router
|
||||||
|
.layer(from_fn(cors_middleware))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
Ok(mint_router)
|
Ok(mint_router)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_bolt12_router(state: MintState) -> Router<MintState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/melt/quote/bolt12", post(post_melt_bolt12_quote))
|
||||||
|
.route(
|
||||||
|
"/melt/quote/bolt12/{quote_id}",
|
||||||
|
get(get_check_melt_bolt11_quote),
|
||||||
|
)
|
||||||
|
.route("/melt/bolt12", post(cache_post_melt_bolt12))
|
||||||
|
.route("/mint/quote/bolt12", post(post_mint_bolt12_quote))
|
||||||
|
.route(
|
||||||
|
"/mint/quote/bolt12/{quote_id}",
|
||||||
|
get(get_check_mint_bolt12_quote),
|
||||||
|
)
|
||||||
|
.route("/mint/bolt12", post(cache_post_mint_bolt12))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use crate::auth::AuthHeader;
|
|||||||
use crate::ws::main_websocket;
|
use crate::ws::main_websocket;
|
||||||
use crate::MintState;
|
use crate::MintState;
|
||||||
|
|
||||||
|
/// Macro to add cache to endpoint
|
||||||
|
#[macro_export]
|
||||||
macro_rules! post_cache_wrapper {
|
macro_rules! post_cache_wrapper {
|
||||||
($handler:ident, $request_type:ty, $response_type:ty) => {
|
($handler:ident, $request_type:ty, $response_type:ty) => {
|
||||||
paste! {
|
paste! {
|
||||||
@@ -163,11 +165,11 @@ pub(crate) async fn post_mint_bolt11_quote(
|
|||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.get_mint_bolt11_quote(payload)
|
.get_mint_quote(payload.into())
|
||||||
.await
|
.await
|
||||||
.map_err(into_response)?;
|
.map_err(into_response)?;
|
||||||
|
|
||||||
Ok(Json(quote))
|
Ok(Json(quote.try_into().map_err(into_response)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "swagger", utoipa::path(
|
#[cfg_attr(feature = "swagger", utoipa::path(
|
||||||
@@ -212,7 +214,7 @@ pub(crate) async fn get_check_mint_bolt11_quote(
|
|||||||
into_response(err)
|
into_response(err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Json(quote))
|
Ok(Json(quote.try_into().map_err(into_response)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@@ -299,7 +301,7 @@ pub(crate) async fn post_melt_bolt11_quote(
|
|||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.get_melt_bolt11_quote(&payload)
|
.get_melt_quote(payload.into())
|
||||||
.await
|
.await
|
||||||
.map_err(into_response)?;
|
.map_err(into_response)?;
|
||||||
|
|
||||||
@@ -382,11 +384,7 @@ pub(crate) async fn post_melt_bolt11(
|
|||||||
.map_err(into_response)?;
|
.map_err(into_response)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = state
|
let res = state.mint.melt(&payload).await.map_err(into_response)?;
|
||||||
.mint
|
|
||||||
.melt_bolt11(&payload)
|
|
||||||
.await
|
|
||||||
.map_err(into_response)?;
|
|
||||||
|
|
||||||
Ok(Json(res))
|
Ok(Json(res))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ rust-version.workspace = true
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["bip353"]
|
||||||
|
bip353 = ["dep:trust-dns-resolver"]
|
||||||
sqlcipher = ["cdk-sqlite/sqlcipher"]
|
sqlcipher = ["cdk-sqlite/sqlcipher"]
|
||||||
# MSRV is not tracked with redb enabled
|
# MSRV is not tracked with redb enabled
|
||||||
redb = ["dep:cdk-redb"]
|
redb = ["dep:cdk-redb"]
|
||||||
@@ -37,3 +39,5 @@ nostr-sdk = { version = "0.41.0", default-features = false, features = [
|
|||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
|
lightning.workspace = true
|
||||||
|
trust-dns-resolver = { version = "0.23.2", optional = true }
|
||||||
|
|||||||
132
crates/cdk-cli/src/bip353.rs
Normal file
132
crates/cdk-cli/src/bip353.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
|
||||||
|
use trust_dns_resolver::TokioAsyncResolver;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Bip353Address {
|
||||||
|
pub user: String,
|
||||||
|
pub domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bip353Address {
|
||||||
|
/// Resolve a human-readable Bitcoin address
|
||||||
|
pub async fn resolve(self) -> Result<PaymentInstruction> {
|
||||||
|
// Construct DNS name
|
||||||
|
let dns_name = format!("{}.user._bitcoin-payment.{}", self.user, self.domain);
|
||||||
|
|
||||||
|
// Create a new resolver with DNSSEC validation
|
||||||
|
let mut opts = ResolverOpts::default();
|
||||||
|
opts.validate = true; // Enable DNSSEC validation
|
||||||
|
|
||||||
|
let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), opts);
|
||||||
|
|
||||||
|
// Query TXT records - with opts.validate=true, this will fail if DNSSEC validation fails
|
||||||
|
let response = resolver.txt_lookup(&dns_name).await?;
|
||||||
|
|
||||||
|
// Extract and concatenate TXT record strings
|
||||||
|
let mut bitcoin_uris = Vec::new();
|
||||||
|
|
||||||
|
for txt in response.iter() {
|
||||||
|
let txt_data: Vec<String> = txt
|
||||||
|
.txt_data()
|
||||||
|
.iter()
|
||||||
|
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let concatenated = txt_data.join("");
|
||||||
|
|
||||||
|
if concatenated.to_lowercase().starts_with("bitcoin:") {
|
||||||
|
bitcoin_uris.push(concatenated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIP-353 requires exactly one Bitcoin URI
|
||||||
|
match bitcoin_uris.len() {
|
||||||
|
0 => bail!("No Bitcoin URI found"),
|
||||||
|
1 => PaymentInstruction::from_uri(&bitcoin_uris[0]),
|
||||||
|
_ => bail!("Multiple Bitcoin URIs found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Bip353Address {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
/// Parse a human-readable Bitcoin address
|
||||||
|
fn from_str(address: &str) -> Result<Self, Self::Err> {
|
||||||
|
let addr = address.trim();
|
||||||
|
|
||||||
|
// Remove Bitcoin prefix if present
|
||||||
|
let addr = addr.strip_prefix("₿").unwrap_or(addr);
|
||||||
|
|
||||||
|
// Split by @
|
||||||
|
let parts: Vec<&str> = addr.split('@').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
bail!("Address is not formatted correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = parts[0].trim();
|
||||||
|
let domain = parts[1].trim();
|
||||||
|
|
||||||
|
if user.is_empty() || domain.is_empty() {
|
||||||
|
bail!("User name and domain must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
user: user.to_string(),
|
||||||
|
domain: domain.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payment instruction type
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum PaymentType {
|
||||||
|
OnChain,
|
||||||
|
LightningOffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BIP-353 payment instruction
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PaymentInstruction {
|
||||||
|
pub parameters: HashMap<PaymentType, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaymentInstruction {
|
||||||
|
/// Parse a payment instruction from a Bitcoin URI
|
||||||
|
pub fn from_uri(uri: &str) -> Result<Self> {
|
||||||
|
if !uri.to_lowercase().starts_with("bitcoin:") {
|
||||||
|
bail!("URI must start with 'bitcoin:'")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parameters = HashMap::new();
|
||||||
|
|
||||||
|
// Parse URI parameters
|
||||||
|
if let Some(query_start) = uri.find('?') {
|
||||||
|
let query = &uri[query_start + 1..];
|
||||||
|
for pair in query.split('&') {
|
||||||
|
if let Some(eq_pos) = pair.find('=') {
|
||||||
|
let key = pair[..eq_pos].to_string();
|
||||||
|
let value = pair[eq_pos + 1..].to_string();
|
||||||
|
let payment_type;
|
||||||
|
// Determine payment type
|
||||||
|
if key.contains("lno") {
|
||||||
|
payment_type = PaymentType::LightningOffer;
|
||||||
|
} else if !uri[8..].contains('?') && uri.len() > 8 {
|
||||||
|
// Simple on-chain address
|
||||||
|
payment_type = PaymentType::OnChain;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.insert(payment_type, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PaymentInstruction { parameters })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ use tracing::Level;
|
|||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(feature = "bip353")]
|
||||||
|
mod bip353;
|
||||||
mod nostr_storage;
|
mod nostr_storage;
|
||||||
mod sub_commands;
|
mod sub_commands;
|
||||||
mod token_storage;
|
mod token_storage;
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use cdk::amount::MSAT_IN_SAT;
|
use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
|
||||||
|
use cdk::mint_url::MintUrl;
|
||||||
use cdk::nuts::{CurrencyUnit, MeltOptions};
|
use cdk::nuts::{CurrencyUnit, MeltOptions};
|
||||||
use cdk::wallet::multi_mint_wallet::MultiMintWallet;
|
use cdk::wallet::multi_mint_wallet::MultiMintWallet;
|
||||||
use cdk::wallet::types::WalletKey;
|
use cdk::wallet::types::WalletKey;
|
||||||
|
use cdk::wallet::{MeltQuote, Wallet};
|
||||||
use cdk::Bolt11Invoice;
|
use cdk::Bolt11Invoice;
|
||||||
use clap::Args;
|
use clap::{Args, ValueEnum};
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
|
use crate::bip353::{Bip353Address, PaymentType as Bip353PaymentType};
|
||||||
use crate::sub_commands::balance::mint_balances;
|
use crate::sub_commands::balance::mint_balances;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
|
get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
|
||||||
validate_mint_number,
|
validate_mint_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
|
pub enum PaymentType {
|
||||||
|
/// BOLT11 invoice
|
||||||
|
Bolt11,
|
||||||
|
/// BOLT12 offer
|
||||||
|
Bolt12,
|
||||||
|
/// Bip353
|
||||||
|
Bip353,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct MeltSubCommand {
|
pub struct MeltSubCommand {
|
||||||
/// Currency unit e.g. sat
|
/// Currency unit e.g. sat
|
||||||
@@ -26,6 +40,56 @@ pub struct MeltSubCommand {
|
|||||||
/// Mint URL to use for melting
|
/// Mint URL to use for melting
|
||||||
#[arg(long, conflicts_with = "mpp")]
|
#[arg(long, conflicts_with = "mpp")]
|
||||||
mint_url: Option<String>,
|
mint_url: Option<String>,
|
||||||
|
/// Payment method (bolt11 or bolt12)
|
||||||
|
#[arg(long, default_value = "bolt11")]
|
||||||
|
method: PaymentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to process a melt quote and execute the payment
|
||||||
|
async fn process_payment(wallet: &Wallet, quote: MeltQuote) -> Result<()> {
|
||||||
|
// Display quote information
|
||||||
|
println!("Quote ID: {}", quote.id);
|
||||||
|
println!("Amount: {}", quote.amount);
|
||||||
|
println!("Fee Reserve: {}", quote.fee_reserve);
|
||||||
|
println!("State: {}", quote.state);
|
||||||
|
println!("Expiry: {}", quote.expiry);
|
||||||
|
|
||||||
|
// Execute the payment
|
||||||
|
let melt = wallet.melt("e.id).await?;
|
||||||
|
println!("Paid: {}", melt.state);
|
||||||
|
|
||||||
|
if let Some(preimage) = melt.preimage {
|
||||||
|
println!("Payment preimage: {preimage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to check if there are enough funds and create appropriate MeltOptions
|
||||||
|
fn create_melt_options(
|
||||||
|
available_funds: u64,
|
||||||
|
payment_amount: Option<u64>,
|
||||||
|
prompt: &str,
|
||||||
|
) -> Result<Option<MeltOptions>> {
|
||||||
|
match payment_amount {
|
||||||
|
Some(amount) => {
|
||||||
|
// Payment has a specified amount
|
||||||
|
if amount > available_funds {
|
||||||
|
bail!("Not enough funds; payment requires {} msats", amount);
|
||||||
|
}
|
||||||
|
Ok(None) // Use default options
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Payment doesn't have an amount, ask user for it
|
||||||
|
let user_amount = get_number_input::<u64>(prompt)? * MSAT_IN_SAT;
|
||||||
|
|
||||||
|
if user_amount > available_funds {
|
||||||
|
bail!("Not enough funds");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(MeltOptions::new_amountless(user_amount)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pay(
|
pub async fn pay(
|
||||||
@@ -35,18 +99,127 @@ pub async fn pay(
|
|||||||
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
|
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
|
||||||
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
|
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
|
||||||
|
|
||||||
let mut mints = vec![];
|
|
||||||
let mut mint_amounts = vec![];
|
|
||||||
if sub_command_args.mpp {
|
if sub_command_args.mpp {
|
||||||
// MPP functionality expects multiple mints, so mint_url flag doesn't fully apply here,
|
// MPP logic only works with BOLT11 currently
|
||||||
// but we can offer to use the specified mint as the first one if provided
|
if !matches!(sub_command_args.method, PaymentType::Bolt11) {
|
||||||
if let Some(mint_url) = &sub_command_args.mint_url {
|
bail!("MPP is only supported for BOLT11 invoices");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect mint numbers and amounts for MPP
|
||||||
|
let (mints, mint_amounts) = collect_mpp_inputs(&mints_amounts, &sub_command_args.mint_url)?;
|
||||||
|
|
||||||
|
// Process BOLT11 MPP payment
|
||||||
|
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
|
||||||
|
|
||||||
|
// Get quotes from all mints
|
||||||
|
let quotes = get_mpp_quotes(
|
||||||
|
multi_mint_wallet,
|
||||||
|
&mints_amounts,
|
||||||
|
&mints,
|
||||||
|
&mint_amounts,
|
||||||
|
&unit,
|
||||||
|
&bolt11,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Execute all melts
|
||||||
|
execute_mpp_melts(quotes).await?;
|
||||||
|
} else {
|
||||||
|
// Get wallet either by mint URL or by index
|
||||||
|
let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
|
||||||
|
// Use the provided mint URL
|
||||||
|
get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await?
|
||||||
|
} else {
|
||||||
|
// Fallback to the index-based selection
|
||||||
|
let mint_number: usize = get_number_input("Enter mint number to melt from")?;
|
||||||
|
get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the mint amount for the selected wallet to check available funds
|
||||||
|
let mint_url = &wallet.mint_url;
|
||||||
|
let mint_amount = mints_amounts
|
||||||
|
.iter()
|
||||||
|
.find(|(url, _)| url == mint_url)
|
||||||
|
.map(|(_, amount)| *amount)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Could not find balance for mint: {}", mint_url))?;
|
||||||
|
|
||||||
|
let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
|
||||||
|
|
||||||
|
// Process payment based on payment method
|
||||||
|
match sub_command_args.method {
|
||||||
|
PaymentType::Bolt11 => {
|
||||||
|
// Process BOLT11 payment
|
||||||
|
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice")?)?;
|
||||||
|
|
||||||
|
// Determine payment amount and options
|
||||||
|
let prompt =
|
||||||
|
"Enter the amount you would like to pay in sats for this amountless invoice.";
|
||||||
|
let options =
|
||||||
|
create_melt_options(available_funds, bolt11.amount_milli_satoshis(), prompt)?;
|
||||||
|
|
||||||
|
// Process payment
|
||||||
|
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
|
||||||
|
process_payment(&wallet, quote).await?;
|
||||||
|
}
|
||||||
|
PaymentType::Bolt12 => {
|
||||||
|
// Process BOLT12 payment (offer)
|
||||||
|
let offer_str = get_user_input("Enter BOLT12 offer")?;
|
||||||
|
let offer = Offer::from_str(&offer_str)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
|
||||||
|
|
||||||
|
// Determine if offer has an amount
|
||||||
|
let prompt =
|
||||||
|
"Enter the amount you would like to pay in sats for this amountless offer:";
|
||||||
|
let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
|
||||||
|
Ok(amount) => Some(u64::from(amount)),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = create_melt_options(available_funds, amount_msat, prompt)?;
|
||||||
|
|
||||||
|
// Get melt quote for BOLT12
|
||||||
|
let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
|
||||||
|
process_payment(&wallet, quote).await?;
|
||||||
|
}
|
||||||
|
PaymentType::Bip353 => {
|
||||||
|
let bip353_addr = get_user_input("Enter Bip353 address.")?;
|
||||||
|
let bip353_addr = Bip353Address::from_str(&bip353_addr)?;
|
||||||
|
|
||||||
|
let payment_instructions = bip353_addr.resolve().await?;
|
||||||
|
|
||||||
|
let offer = payment_instructions
|
||||||
|
.parameters
|
||||||
|
.get(&Bip353PaymentType::LightningOffer)
|
||||||
|
.ok_or(anyhow!("Offer not defined"))?;
|
||||||
|
|
||||||
|
let prompt =
|
||||||
|
"Enter the amount you would like to pay in sats for this amountless offer:";
|
||||||
|
// BIP353 payments are always amountless for now
|
||||||
|
let options = create_melt_options(available_funds, None, prompt)?;
|
||||||
|
|
||||||
|
// Get melt quote for BOLT12
|
||||||
|
let quote = wallet.melt_bolt12_quote(offer.to_string(), options).await?;
|
||||||
|
process_payment(&wallet, quote).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect mint numbers and amounts for MPP payments
|
||||||
|
fn collect_mpp_inputs(
|
||||||
|
mints_amounts: &[(MintUrl, Amount)],
|
||||||
|
mint_url_opt: &Option<String>,
|
||||||
|
) -> Result<(Vec<usize>, Vec<u64>)> {
|
||||||
|
let mut mints = Vec::new();
|
||||||
|
let mut mint_amounts = Vec::new();
|
||||||
|
|
||||||
|
// If a specific mint URL was provided, try to use it as the first mint
|
||||||
|
if let Some(mint_url) = mint_url_opt {
|
||||||
println!("Using mint URL {mint_url} as the first mint for MPP payment.");
|
println!("Using mint URL {mint_url} as the first mint for MPP payment.");
|
||||||
|
|
||||||
// Check if the mint exists
|
|
||||||
if let Ok(_wallet) =
|
|
||||||
get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await
|
|
||||||
{
|
|
||||||
// Find the index of this mint in the mints_amounts list
|
// Find the index of this mint in the mints_amounts list
|
||||||
if let Some(mint_index) = mints_amounts
|
if let Some(mint_index) = mints_amounts
|
||||||
.iter()
|
.iter()
|
||||||
@@ -57,12 +230,13 @@ pub async fn pay(
|
|||||||
get_number_input("Enter amount to mint from this mint in sats.")?;
|
get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||||
mint_amounts.push(melt_amount);
|
mint_amounts.push(melt_amount);
|
||||||
} else {
|
} else {
|
||||||
println!("Warning: Mint URL exists but no balance found. Continuing with manual selection.");
|
println!(
|
||||||
}
|
"Warning: Mint URL not found or no balance. Continuing with manual selection."
|
||||||
} else {
|
);
|
||||||
println!("Warning: Could not find wallet for the specified mint URL. Continuing with manual selection.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue with regular mint selection
|
||||||
loop {
|
loop {
|
||||||
let mint_number: String =
|
let mint_number: String =
|
||||||
get_user_input("Enter mint number to melt from and -1 when done.")?;
|
get_user_input("Enter mint number to melt from and -1 when done.")?;
|
||||||
@@ -75,13 +249,26 @@ pub async fn pay(
|
|||||||
validate_mint_number(mint_number, mints_amounts.len())?;
|
validate_mint_number(mint_number, mints_amounts.len())?;
|
||||||
|
|
||||||
mints.push(mint_number);
|
mints.push(mint_number);
|
||||||
let melt_amount: u64 =
|
let melt_amount: u64 = get_number_input("Enter amount to mint from this mint in sats.")?;
|
||||||
get_number_input("Enter amount to mint from this mint in sats.")?;
|
|
||||||
mint_amounts.push(melt_amount);
|
mint_amounts.push(melt_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
|
if mints.is_empty() {
|
||||||
|
bail!("No mints selected for MPP payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((mints, mint_amounts))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get quotes from all mints for MPP payment
|
||||||
|
async fn get_mpp_quotes(
|
||||||
|
multi_mint_wallet: &MultiMintWallet,
|
||||||
|
mints_amounts: &[(MintUrl, Amount)],
|
||||||
|
mints: &[usize],
|
||||||
|
mint_amounts: &[u64],
|
||||||
|
unit: &CurrencyUnit,
|
||||||
|
bolt11: &Bolt11Invoice,
|
||||||
|
) -> Result<Vec<(Wallet, MeltQuote)>> {
|
||||||
let mut quotes = JoinSet::new();
|
let mut quotes = JoinSet::new();
|
||||||
|
|
||||||
for (mint, amount) in mints.iter().zip(mint_amounts) {
|
for (mint, amount) in mints.iter().zip(mint_amounts) {
|
||||||
@@ -91,7 +278,7 @@ pub async fn pay(
|
|||||||
.get_wallet(&WalletKey::new(wallet, unit.clone()))
|
.get_wallet(&WalletKey::new(wallet, unit.clone()))
|
||||||
.await
|
.await
|
||||||
.expect("Known wallet");
|
.expect("Known wallet");
|
||||||
let options = MeltOptions::new_mpp(amount * 1000);
|
let options = MeltOptions::new_mpp(*amount * 1000);
|
||||||
|
|
||||||
let bolt11_clone = bolt11.clone();
|
let bolt11_clone = bolt11.clone();
|
||||||
|
|
||||||
@@ -104,26 +291,34 @@ pub async fn pay(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let quotes = quotes.join_all().await;
|
let quotes_results = quotes.join_all().await;
|
||||||
|
|
||||||
for (wallet, quote) in quotes.iter() {
|
// Validate all quotes succeeded
|
||||||
if let Err(quote) = quote {
|
let mut valid_quotes = Vec::new();
|
||||||
tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote);
|
for (wallet, quote_result) in quotes_results {
|
||||||
bail!("Could not get melt quote for {}", wallet.mint_url);
|
match quote_result {
|
||||||
} else {
|
Ok(quote) => {
|
||||||
let quote = quote.as_ref().unwrap();
|
|
||||||
println!(
|
println!(
|
||||||
"Melt quote {} for mint {} of amount {} with fee {}.",
|
"Melt quote {} for mint {} of amount {} with fee {}.",
|
||||||
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
|
quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
|
||||||
);
|
);
|
||||||
|
valid_quotes.push((wallet, quote));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, err);
|
||||||
|
bail!("Could not get melt quote for {}", wallet.mint_url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(valid_quotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute all melts for MPP payment
|
||||||
|
async fn execute_mpp_melts(quotes: Vec<(Wallet, MeltQuote)>) -> Result<()> {
|
||||||
let mut melts = JoinSet::new();
|
let mut melts = JoinSet::new();
|
||||||
|
|
||||||
for (wallet, quote) in quotes {
|
for (wallet, quote) in quotes {
|
||||||
let quote = quote.expect("Errors checked above");
|
|
||||||
|
|
||||||
melts.spawn(async move {
|
melts.spawn(async move {
|
||||||
let melt = wallet.melt("e.id).await;
|
let melt = wallet.melt("e.id).await;
|
||||||
(wallet, melt)
|
(wallet, melt)
|
||||||
@@ -152,69 +347,6 @@ pub async fn pay(
|
|||||||
if error {
|
if error {
|
||||||
bail!("Could not complete all melts");
|
bail!("Could not complete all melts");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Get wallet either by mint URL or by index
|
|
||||||
let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
|
|
||||||
// Use the provided mint URL
|
|
||||||
get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await?
|
|
||||||
} else {
|
|
||||||
// Fallback to the index-based selection
|
|
||||||
let mint_number: usize = get_number_input("Enter mint number to melt from")?;
|
|
||||||
get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the mint amount for the selected wallet to check available funds
|
|
||||||
let mint_url = &wallet.mint_url;
|
|
||||||
let mint_amount = mints_amounts
|
|
||||||
.iter()
|
|
||||||
.find(|(url, _)| url == mint_url)
|
|
||||||
.map(|(_, amount)| *amount)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Could not find balance for mint: {}", mint_url))?;
|
|
||||||
|
|
||||||
let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
|
|
||||||
|
|
||||||
let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
|
|
||||||
|
|
||||||
// Determine payment amount and options
|
|
||||||
let options = if bolt11.amount_milli_satoshis().is_none() {
|
|
||||||
// Get user input for amount
|
|
||||||
let prompt = format!(
|
|
||||||
"Enter the amount you would like to pay in sats for a {} payment.",
|
|
||||||
if sub_command_args.mpp {
|
|
||||||
"MPP"
|
|
||||||
} else {
|
|
||||||
"amountless invoice"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let user_amount = get_number_input::<u64>(&prompt)? * MSAT_IN_SAT;
|
|
||||||
|
|
||||||
if user_amount > available_funds {
|
|
||||||
bail!("Not enough funds");
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(MeltOptions::new_amountless(user_amount))
|
|
||||||
} else {
|
|
||||||
// Check if invoice amount exceeds available funds
|
|
||||||
let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
|
|
||||||
if invoice_amount > available_funds {
|
|
||||||
bail!("Not enough funds");
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process payment
|
|
||||||
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
|
|
||||||
println!("{quote:?}");
|
|
||||||
|
|
||||||
let melt = wallet.melt("e.id).await?;
|
|
||||||
println!("Paid invoice: {}", melt.state);
|
|
||||||
|
|
||||||
if let Some(preimage) = melt.preimage {
|
|
||||||
println!("Payment preimage: {preimage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
|
|||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
use cdk::mint_url::MintUrl;
|
use cdk::mint_url::MintUrl;
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
|
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, PaymentMethod};
|
||||||
use cdk::wallet::{MultiMintWallet, WalletSubscription};
|
use cdk::wallet::{MultiMintWallet, WalletSubscription};
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
@@ -27,6 +27,15 @@ pub struct MintSubCommand {
|
|||||||
/// Quote Id
|
/// Quote Id
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
quote_id: Option<String>,
|
quote_id: Option<String>,
|
||||||
|
/// Payment method
|
||||||
|
#[arg(long, default_value = "bolt11")]
|
||||||
|
method: String,
|
||||||
|
/// Expiry
|
||||||
|
#[arg(short, long)]
|
||||||
|
expiry: Option<u64>,
|
||||||
|
/// Expiry
|
||||||
|
#[arg(short, long)]
|
||||||
|
single_use: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mint(
|
pub async fn mint(
|
||||||
@@ -39,8 +48,11 @@ pub async fn mint(
|
|||||||
|
|
||||||
let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
|
let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?;
|
||||||
|
|
||||||
|
let mut payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
|
||||||
|
|
||||||
let quote_id = match &sub_command_args.quote_id {
|
let quote_id = match &sub_command_args.quote_id {
|
||||||
None => {
|
None => match payment_method {
|
||||||
|
PaymentMethod::Bolt11 => {
|
||||||
let amount = sub_command_args
|
let amount = sub_command_args
|
||||||
.amount
|
.amount
|
||||||
.ok_or(anyhow!("Amount must be defined"))?;
|
.ok_or(anyhow!("Amount must be defined"))?;
|
||||||
@@ -65,10 +77,75 @@ pub async fn mint(
|
|||||||
}
|
}
|
||||||
quote.id
|
quote.id
|
||||||
}
|
}
|
||||||
Some(quote_id) => quote_id.to_string(),
|
PaymentMethod::Bolt12 => {
|
||||||
|
let amount = sub_command_args.amount;
|
||||||
|
println!("{:?}", sub_command_args.single_use);
|
||||||
|
let quote = wallet
|
||||||
|
.mint_bolt12_quote(amount.map(|a| a.into()), description)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Quote: {quote:#?}");
|
||||||
|
|
||||||
|
println!("Please pay: {}", quote.request);
|
||||||
|
|
||||||
|
let mut subscription = wallet
|
||||||
|
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
|
||||||
|
.id
|
||||||
|
.clone()]))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
while let Some(msg) = subscription.recv().await {
|
||||||
|
if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
|
||||||
|
if response.state == MintQuoteState::Paid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quote.id
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(quote_id) => {
|
||||||
|
let quote = wallet
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(quote_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(anyhow!("Unknown quote"))?;
|
||||||
|
|
||||||
|
payment_method = quote.payment_method;
|
||||||
|
quote_id.to_string()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let proofs = wallet.mint("e_id, SplitTarget::default(), None).await?;
|
tracing::debug!("Attempting mint for: {}", payment_method);
|
||||||
|
|
||||||
|
let proofs = match payment_method {
|
||||||
|
PaymentMethod::Bolt11 => wallet.mint("e_id, SplitTarget::default(), None).await?,
|
||||||
|
PaymentMethod::Bolt12 => {
|
||||||
|
let response = wallet.mint_bolt12_quote_state("e_id).await?;
|
||||||
|
|
||||||
|
let amount_mintable = response.amount_paid - response.amount_issued;
|
||||||
|
|
||||||
|
if amount_mintable == Amount::ZERO {
|
||||||
|
println!("Mint quote does not have amount that can be minted.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.mint_bolt12(
|
||||||
|
"e_id,
|
||||||
|
Some(amount_mintable),
|
||||||
|
SplitTarget::default(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let receive_amount = proofs.total_amount()?;
|
let receive_amount = proofs.total_amount()?;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ pub enum Error {
|
|||||||
/// Amount Error
|
/// Amount Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Amount(#[from] cdk_common::amount::Error),
|
Amount(#[from] cdk_common::amount::Error),
|
||||||
|
/// UTF-8 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
|
/// Bolt12 Error
|
||||||
|
#[error("Bolt12 error: {0}")]
|
||||||
|
Bolt12(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for cdk_common::payment::Error {
|
impl From<Error> for cdk_common::payment::Error {
|
||||||
|
|||||||
@@ -10,29 +10,35 @@ use std::pin::Pin;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use bitcoin::hashes::sha256::Hash;
|
||||||
use cdk_common::amount::{to_unit, Amount};
|
use cdk_common::amount::{to_unit, Amount};
|
||||||
use cdk_common::common::FeeReserve;
|
use cdk_common::common::FeeReserve;
|
||||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
|
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||||
use cdk_common::payment::{
|
use cdk_common::payment::{
|
||||||
self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
|
self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
|
||||||
PaymentQuoteResponse,
|
CreateIncomingPaymentResponse, IncomingPaymentOptions, MakePaymentResponse, MintPayment,
|
||||||
|
OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk_common::util::{hex, unix_time};
|
use cdk_common::util::{hex, unix_time};
|
||||||
use cdk_common::{mint, Bolt11Invoice};
|
use cdk_common::Bolt11Invoice;
|
||||||
use cln_rpc::model::requests::{
|
use cln_rpc::model::requests::{
|
||||||
InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest,
|
DecodeRequest, FetchinvoiceRequest, InvoiceRequest, ListinvoicesRequest, ListpaysRequest,
|
||||||
|
OfferRequest, PayRequest, WaitanyinvoiceRequest,
|
||||||
};
|
};
|
||||||
use cln_rpc::model::responses::{
|
use cln_rpc::model::responses::{
|
||||||
ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus,
|
DecodeResponse, ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus,
|
||||||
WaitanyinvoiceStatus,
|
PayStatus, WaitanyinvoiceResponse, WaitanyinvoiceStatus,
|
||||||
};
|
};
|
||||||
use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
|
use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny, Sha256};
|
||||||
|
use cln_rpc::ClnRpc;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -68,6 +74,7 @@ impl MintPayment for Cln {
|
|||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
amountless: true,
|
amountless: true,
|
||||||
|
bolt12: true,
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +88,33 @@ impl MintPayment for Cln {
|
|||||||
self.wait_invoice_cancel_token.cancel()
|
self.wait_invoice_cancel_token.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn wait_any_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||||
let last_pay_index = self.get_last_pay_index().await?;
|
tracing::info!(
|
||||||
let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
"CLN: Starting wait_any_incoming_payment with socket: {:?}",
|
||||||
|
self.rpc_socket
|
||||||
|
);
|
||||||
|
|
||||||
|
let last_pay_index = self.get_last_pay_index().await?.map(|idx| {
|
||||||
|
tracing::info!("CLN: Found last payment index: {}", idx);
|
||||||
|
idx
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::debug!("CLN: Connecting to CLN node...");
|
||||||
|
let cln_client = match cln_rpc::ClnRpc::new(&self.rpc_socket).await {
|
||||||
|
Ok(client) => {
|
||||||
|
tracing::debug!("CLN: Successfully connected to CLN node");
|
||||||
|
client
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("CLN: Failed to connect to CLN node: {}", err);
|
||||||
|
return Err(Error::from(err).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!("CLN: Creating stream processing pipeline");
|
||||||
let stream = futures::stream::unfold(
|
let stream = futures::stream::unfold(
|
||||||
(
|
(
|
||||||
cln_client,
|
cln_client,
|
||||||
@@ -97,70 +125,133 @@ impl MintPayment for Cln {
|
|||||||
|(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move {
|
|(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move {
|
||||||
// Set the stream as active
|
// Set the stream as active
|
||||||
is_active.store(true, Ordering::SeqCst);
|
is_active.store(true, Ordering::SeqCst);
|
||||||
|
tracing::debug!("CLN: Stream is now active, waiting for invoice events with lastpay_index: {:?}", last_pay_idx);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let request = WaitanyinvoiceRequest {
|
|
||||||
timeout: None,
|
|
||||||
lastpay_index: last_pay_idx,
|
|
||||||
};
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel_token.cancelled() => {
|
_ = cancel_token.cancelled() => {
|
||||||
// Set the stream as inactive
|
// Set the stream as inactive
|
||||||
is_active.store(false, Ordering::SeqCst);
|
is_active.store(false, Ordering::SeqCst);
|
||||||
|
tracing::info!("CLN: Invoice stream cancelled");
|
||||||
// End the stream
|
// End the stream
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
result = cln_client.call_typed(&request) => {
|
result = cln_client.call(cln_rpc::Request::WaitAnyInvoice(WaitanyinvoiceRequest {
|
||||||
|
timeout: None,
|
||||||
|
lastpay_index: last_pay_idx,
|
||||||
|
})) => {
|
||||||
|
tracing::debug!("CLN: Received response from WaitAnyInvoice call");
|
||||||
match result {
|
match result {
|
||||||
Ok(invoice) => {
|
Ok(invoice) => {
|
||||||
|
tracing::debug!("CLN: Successfully received invoice data");
|
||||||
|
// Try to convert the invoice to WaitanyinvoiceResponse
|
||||||
|
let wait_any_response_result: Result<WaitanyinvoiceResponse, _> =
|
||||||
|
invoice.try_into();
|
||||||
|
|
||||||
|
let wait_any_response = match wait_any_response_result {
|
||||||
|
Ok(response) => {
|
||||||
|
tracing::debug!("CLN: Parsed WaitAnyInvoice response successfully");
|
||||||
|
response
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"CLN: Failed to parse WaitAnyInvoice response: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
// Continue to the next iteration without panicking
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check the status of the invoice
|
// Check the status of the invoice
|
||||||
// We only want to yield invoices that have been paid
|
// We only want to yield invoices that have been paid
|
||||||
match invoice.status {
|
match wait_any_response.status {
|
||||||
WaitanyinvoiceStatus::PAID => (),
|
WaitanyinvoiceStatus::PAID => {
|
||||||
WaitanyinvoiceStatus::EXPIRED => continue,
|
tracing::info!("CLN: Invoice with payment index {} is PAID",
|
||||||
|
wait_any_response.pay_index.unwrap_or_default());
|
||||||
|
}
|
||||||
|
WaitanyinvoiceStatus::EXPIRED => {
|
||||||
|
tracing::debug!("CLN: Invoice with payment index {} is EXPIRED, skipping",
|
||||||
|
wait_any_response.pay_index.unwrap_or_default());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
last_pay_idx = invoice.pay_index;
|
last_pay_idx = wait_any_response.pay_index;
|
||||||
|
tracing::debug!("CLN: Updated last_pay_idx to {:?}", last_pay_idx);
|
||||||
|
|
||||||
let payment_hash = invoice.payment_hash.to_string();
|
let payment_hash = wait_any_response.payment_hash;
|
||||||
|
tracing::debug!("CLN: Payment hash: {}", payment_hash);
|
||||||
|
|
||||||
let request_look_up = match invoice.bolt12 {
|
let amount_msats = match wait_any_response.amount_received_msat {
|
||||||
|
Some(amt) => {
|
||||||
|
tracing::info!("CLN: Received payment of {} msats for {}",
|
||||||
|
amt.msat(), payment_hash);
|
||||||
|
amt
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::error!("CLN: No amount in paid invoice, this should not happen");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let amount_sats = amount_msats.msat() / 1000;
|
||||||
|
|
||||||
|
let payment_hash = Hash::from_bytes_ref(payment_hash.as_ref());
|
||||||
|
|
||||||
|
let request_lookup_id = match wait_any_response.bolt12 {
|
||||||
// If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up.
|
// If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up.
|
||||||
// Since this is not returned in the wait any response,
|
// Since this is not returned in the wait any response,
|
||||||
// we need to do a second query for it.
|
// we need to do a second query for it.
|
||||||
Some(_) => {
|
Some(bolt12) => {
|
||||||
|
tracing::info!("CLN: Processing BOLT12 payment, bolt12 value: {}", bolt12);
|
||||||
match fetch_invoice_by_payment_hash(
|
match fetch_invoice_by_payment_hash(
|
||||||
&mut cln_client,
|
&mut cln_client,
|
||||||
&payment_hash,
|
payment_hash,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(invoice)) => {
|
Ok(Some(invoice)) => {
|
||||||
if let Some(local_offer_id) = invoice.local_offer_id {
|
if let Some(local_offer_id) = invoice.local_offer_id {
|
||||||
local_offer_id.to_string()
|
tracing::info!("CLN: Received bolt12 payment of {} sats for offer {}",
|
||||||
|
amount_sats, local_offer_id);
|
||||||
|
PaymentIdentifier::OfferId(local_offer_id.to_string())
|
||||||
} else {
|
} else {
|
||||||
|
tracing::warn!("CLN: BOLT12 invoice has no local_offer_id, skipping");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => continue,
|
Ok(None) => {
|
||||||
|
tracing::warn!("CLN: Failed to find invoice by payment hash, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Error fetching invoice by payment hash: {e}"
|
"CLN: Error fetching invoice by payment hash: {e}"
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => payment_hash,
|
None => {
|
||||||
|
tracing::info!("CLN: Processing BOLT11 payment with hash {}", payment_hash);
|
||||||
|
PaymentIdentifier::PaymentHash(*payment_hash.as_ref())
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active)));
|
let response = WaitPaymentResponse {
|
||||||
|
payment_identifier: request_lookup_id,
|
||||||
|
payment_amount: amount_sats.into(),
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
payment_id: payment_hash.to_string()
|
||||||
|
};
|
||||||
|
tracing::info!("CLN: Created WaitPaymentResponse with amount {} sats", amount_sats);
|
||||||
|
|
||||||
|
break Some((response, (cln_client, last_pay_idx, cancel_token, is_active)));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Error fetching invoice: {e}");
|
tracing::warn!("CLN: Error fetching invoice: {e}");
|
||||||
is_active.store(false, Ordering::SeqCst);
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
return None;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,80 +261,199 @@ impl MintPayment for Cln {
|
|||||||
)
|
)
|
||||||
.boxed();
|
.boxed();
|
||||||
|
|
||||||
|
tracing::info!("CLN: Successfully initialized invoice stream");
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn get_payment_quote(
|
async fn get_payment_quote(
|
||||||
&self,
|
&self,
|
||||||
request: &str,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
options: Option<MeltOptions>,
|
options: OutgoingPaymentOptions,
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||||
let bolt11 = Bolt11Invoice::from_str(request)?;
|
match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
// If we have specific amount options, use those
|
||||||
|
let amount_msat: Amount = if let Some(melt_options) = bolt11_options.melt_options {
|
||||||
|
match melt_options {
|
||||||
|
MeltOptions::Amountless { amountless } => {
|
||||||
|
let amount_msat = amountless.amount_msat;
|
||||||
|
|
||||||
let amount_msat = match options {
|
if let Some(invoice_amount) =
|
||||||
Some(amount) => amount.amount_msat(),
|
bolt11_options.bolt11.amount_milli_satoshis()
|
||||||
None => bolt11
|
{
|
||||||
|
if !invoice_amount == u64::from(amount_msat) {
|
||||||
|
return Err(payment::Error::AmountMismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
amount_msat
|
||||||
|
}
|
||||||
|
MeltOptions::Mpp { mpp } => mpp.amount,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to invoice amount
|
||||||
|
bolt11_options
|
||||||
|
.bolt11
|
||||||
.amount_milli_satoshis()
|
.amount_milli_satoshis()
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
.into(),
|
.into()
|
||||||
};
|
};
|
||||||
|
// Convert to target unit
|
||||||
let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
|
let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
|
||||||
|
|
||||||
|
// Calculate fee
|
||||||
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;
|
||||||
|
|
||||||
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
|
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
|
||||||
|
|
||||||
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
||||||
|
|
||||||
Ok(PaymentQuoteResponse {
|
Ok(PaymentQuoteResponse {
|
||||||
request_lookup_id: bolt11.payment_hash().to_string(),
|
request_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
*bolt11_options.bolt11.payment_hash().as_ref(),
|
||||||
|
),
|
||||||
amount,
|
amount,
|
||||||
unit: unit.clone(),
|
|
||||||
fee: fee.into(),
|
fee: fee.into(),
|
||||||
state: MeltQuoteState::Unpaid,
|
state: MeltQuoteState::Unpaid,
|
||||||
|
options: None,
|
||||||
|
unit: unit.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let offer = bolt12_options.offer;
|
||||||
|
|
||||||
|
let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
|
||||||
|
amount.amount_msat().into()
|
||||||
|
} else {
|
||||||
|
// Fall back to offer amount
|
||||||
|
let decode_response = self.decode_string(offer.to_string()).await?;
|
||||||
|
|
||||||
|
decode_response
|
||||||
|
.offer_amount_msat
|
||||||
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
|
.msat()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to target unit
|
||||||
|
let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
|
||||||
|
|
||||||
|
// Calculate fee
|
||||||
|
let relative_fee_reserve =
|
||||||
|
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
|
||||||
|
let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
|
||||||
|
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
||||||
|
|
||||||
|
let cln_response;
|
||||||
|
{
|
||||||
|
// Fetch invoice from offer
|
||||||
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
|
cln_response = cln_client
|
||||||
|
.call_typed(&FetchinvoiceRequest {
|
||||||
|
amount_msat: Some(CLN_Amount::from_msat(amount_msat)),
|
||||||
|
payer_metadata: None,
|
||||||
|
payer_note: None,
|
||||||
|
quantity: None,
|
||||||
|
recurrence_counter: None,
|
||||||
|
recurrence_label: None,
|
||||||
|
recurrence_start: None,
|
||||||
|
timeout: None,
|
||||||
|
offer: offer.to_string(),
|
||||||
|
bip353: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Could not fetch invoice for offer: {:?}", err);
|
||||||
|
Error::ClnRpc(err)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decode_response = self.decode_string(cln_response.invoice.clone()).await?;
|
||||||
|
|
||||||
|
let options = payment::PaymentQuoteOptions::Bolt12 {
|
||||||
|
invoice: Some(cln_response.invoice.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PaymentQuoteResponse {
|
||||||
|
request_lookup_id: PaymentIdentifier::Bolt12PaymentHash(
|
||||||
|
hex::decode(
|
||||||
|
decode_response
|
||||||
|
.invoice_payment_hash
|
||||||
|
.ok_or(Error::UnknownInvoice)?,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidHash)?,
|
||||||
|
),
|
||||||
|
amount,
|
||||||
|
fee: fee.into(),
|
||||||
|
state: MeltQuoteState::Unpaid,
|
||||||
|
options: Some(options),
|
||||||
|
unit: unit.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
unit: &CurrencyUnit,
|
||||||
partial_amount: Option<Amount>,
|
options: OutgoingPaymentOptions,
|
||||||
max_fee: Option<Amount>,
|
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
|
let max_fee_msat: Option<u64>;
|
||||||
let pay_state = self
|
let mut partial_amount: Option<u64> = None;
|
||||||
.check_outgoing_payment(&bolt11.payment_hash().to_string())
|
let mut amount_msat: Option<u64> = None;
|
||||||
|
|
||||||
|
let invoice = match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let payment_identifier =
|
||||||
|
PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
|
||||||
|
|
||||||
|
self.check_outgoing_unpaided(&payment_identifier).await?;
|
||||||
|
|
||||||
|
if let Some(melt_options) = bolt11_options.melt_options {
|
||||||
|
match melt_options {
|
||||||
|
MeltOptions::Mpp { mpp } => partial_amount = Some(mpp.amount.into()),
|
||||||
|
MeltOptions::Amountless { amountless } => {
|
||||||
|
amount_msat = Some(amountless.amount_msat.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
max_fee_msat = bolt11_options.max_fee_amount.map(|a| a.into());
|
||||||
|
|
||||||
|
bolt11_options.bolt11.to_string()
|
||||||
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let bolt12_invoice = bolt12_options.invoice.ok_or(Error::UnknownInvoice)?;
|
||||||
|
let decode_response = self
|
||||||
|
.decode_string(String::from_utf8(bolt12_invoice.clone()).map_err(Error::Utf8)?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match pay_state.status {
|
let payment_identifier = PaymentIdentifier::Bolt12PaymentHash(
|
||||||
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
|
hex::decode(
|
||||||
MeltQuoteState::Paid => {
|
decode_response
|
||||||
tracing::debug!("Melt attempted on invoice already paid");
|
.invoice_payment_hash
|
||||||
return Err(Self::Err::InvoiceAlreadyPaid);
|
.ok_or(Error::UnknownInvoice)?,
|
||||||
}
|
)
|
||||||
MeltQuoteState::Pending => {
|
.map_err(|e| Error::Bolt12(e.to_string()))?
|
||||||
tracing::debug!("Melt attempted on invoice already pending");
|
.try_into()
|
||||||
return Err(Self::Err::InvoicePaymentPending);
|
.map_err(|_| Error::InvalidHash)?,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let amount_msat = partial_amount
|
self.check_outgoing_unpaided(&payment_identifier).await?;
|
||||||
.is_none()
|
|
||||||
.then(|| {
|
max_fee_msat = bolt12_options.max_fee_amount.map(|a| a.into());
|
||||||
melt_quote
|
String::from_utf8(bolt12_invoice).map_err(Error::Utf8)?
|
||||||
.msat_to_pay
|
}
|
||||||
.map(|a| CLN_Amount::from_msat(a.into()))
|
};
|
||||||
})
|
|
||||||
.flatten();
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
|
||||||
let cln_response = cln_client
|
let cln_response = cln_client
|
||||||
.call_typed(&PayRequest {
|
.call_typed(&PayRequest {
|
||||||
bolt11: melt_quote.request.to_string(),
|
bolt11: invoice,
|
||||||
amount_msat,
|
amount_msat: amount_msat.map(CLN_Amount::from_msat),
|
||||||
label: None,
|
label: None,
|
||||||
riskfactor: None,
|
riskfactor: None,
|
||||||
maxfeepercent: None,
|
maxfeepercent: None,
|
||||||
@@ -252,22 +462,9 @@ impl MintPayment for Cln {
|
|||||||
exemptfee: None,
|
exemptfee: None,
|
||||||
localinvreqid: None,
|
localinvreqid: None,
|
||||||
exclude: None,
|
exclude: None,
|
||||||
maxfee: max_fee
|
maxfee: max_fee_msat.map(CLN_Amount::from_msat),
|
||||||
.map(|a| {
|
|
||||||
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
|
|
||||||
Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
|
|
||||||
})
|
|
||||||
.transpose()?,
|
|
||||||
description: None,
|
description: None,
|
||||||
partial_msat: partial_amount
|
partial_msat: partial_amount.map(CLN_Amount::from_msat),
|
||||||
.map(|a| {
|
|
||||||
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
|
|
||||||
|
|
||||||
Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
|
|
||||||
msat.into(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.transpose()?,
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -279,16 +476,19 @@ impl MintPayment for Cln {
|
|||||||
PayStatus::FAILED => MeltQuoteState::Failed,
|
PayStatus::FAILED => MeltQuoteState::Failed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let payment_identifier =
|
||||||
|
PaymentIdentifier::PaymentHash(*pay_response.payment_hash.as_ref());
|
||||||
|
|
||||||
MakePaymentResponse {
|
MakePaymentResponse {
|
||||||
payment_proof: 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: payment_identifier,
|
||||||
status,
|
status,
|
||||||
total_spent: to_unit(
|
total_spent: to_unit(
|
||||||
pay_response.amount_sent_msat.msat(),
|
pay_response.amount_sent_msat.msat(),
|
||||||
&CurrencyUnit::Msat,
|
&CurrencyUnit::Msat,
|
||||||
&melt_quote.unit,
|
unit,
|
||||||
)?,
|
)?,
|
||||||
unit: melt_quote.unit,
|
unit: unit.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -300,16 +500,21 @@ impl MintPayment for Cln {
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
description: String,
|
options: IncomingPaymentOptions,
|
||||||
unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
||||||
|
match options {
|
||||||
|
IncomingPaymentOptions::Bolt11(Bolt11IncomingPaymentOptions {
|
||||||
|
description,
|
||||||
|
amount,
|
||||||
|
unix_expiry,
|
||||||
|
}) => {
|
||||||
let time_now = unix_time();
|
let time_now = unix_time();
|
||||||
|
|
||||||
let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
let label = Uuid::new_v4().to_string();
|
let label = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
@@ -319,7 +524,7 @@ impl MintPayment for Cln {
|
|||||||
let invoice_response = cln_client
|
let invoice_response = cln_client
|
||||||
.call_typed(&InvoiceRequest {
|
.call_typed(&InvoiceRequest {
|
||||||
amount_msat,
|
amount_msat,
|
||||||
description,
|
description: description.unwrap_or_default(),
|
||||||
label: label.clone(),
|
label: label.clone(),
|
||||||
expiry: unix_expiry.map(|t| t - time_now),
|
expiry: unix_expiry.map(|t| t - time_now),
|
||||||
fallbacks: None,
|
fallbacks: None,
|
||||||
@@ -336,21 +541,107 @@ impl MintPayment for Cln {
|
|||||||
let payment_hash = request.payment_hash();
|
let payment_hash = request.payment_hash();
|
||||||
|
|
||||||
Ok(CreateIncomingPaymentResponse {
|
Ok(CreateIncomingPaymentResponse {
|
||||||
request_lookup_id: payment_hash.to_string(),
|
request_lookup_id: PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
|
||||||
request: request.to_string(),
|
request: request.to_string(),
|
||||||
expiry,
|
expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
IncomingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let Bolt12IncomingPaymentOptions {
|
||||||
|
description,
|
||||||
|
amount,
|
||||||
|
unix_expiry,
|
||||||
|
} = *bolt12_options;
|
||||||
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
|
let label = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Match like this until we change to option
|
||||||
|
let amount = match amount {
|
||||||
|
Some(amount) => {
|
||||||
|
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
||||||
|
|
||||||
|
amount.to_string()
|
||||||
|
}
|
||||||
|
None => "any".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// It seems that the only way to force cln to create a unique offer
|
||||||
|
// is to encode some random data in the offer
|
||||||
|
let issuer = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let offer_response = cln_client
|
||||||
|
.call_typed(&OfferRequest {
|
||||||
|
amount,
|
||||||
|
absolute_expiry: unix_expiry,
|
||||||
|
description: Some(description.unwrap_or_default()),
|
||||||
|
issuer: Some(issuer.to_string()),
|
||||||
|
label: Some(label.to_string()),
|
||||||
|
single_use: None,
|
||||||
|
quantity_max: None,
|
||||||
|
recurrence: None,
|
||||||
|
recurrence_base: None,
|
||||||
|
recurrence_limit: None,
|
||||||
|
recurrence_paywindow: None,
|
||||||
|
recurrence_start_any_period: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(CreateIncomingPaymentResponse {
|
||||||
|
request_lookup_id: PaymentIdentifier::OfferId(
|
||||||
|
offer_response.offer_id.to_string(),
|
||||||
|
),
|
||||||
|
request: offer_response.bolt12,
|
||||||
|
expiry: unix_expiry,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
payment_hash: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err> {
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
|
||||||
let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
let listinvoices_response = cln_client
|
let listinvoices_response = match payment_identifier {
|
||||||
|
PaymentIdentifier::Label(label) => {
|
||||||
|
// Query by label
|
||||||
|
cln_client
|
||||||
.call_typed(&ListinvoicesRequest {
|
.call_typed(&ListinvoicesRequest {
|
||||||
payment_hash: Some(payment_hash.to_string()),
|
payment_hash: None,
|
||||||
|
label: Some(label.to_string()),
|
||||||
|
invstring: None,
|
||||||
|
offer_id: None,
|
||||||
|
index: None,
|
||||||
|
limit: None,
|
||||||
|
start: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?
|
||||||
|
}
|
||||||
|
PaymentIdentifier::OfferId(offer_id) => {
|
||||||
|
// Query by offer_id
|
||||||
|
cln_client
|
||||||
|
.call_typed(&ListinvoicesRequest {
|
||||||
|
payment_hash: None,
|
||||||
|
label: None,
|
||||||
|
invstring: None,
|
||||||
|
offer_id: Some(offer_id.to_string()),
|
||||||
|
index: None,
|
||||||
|
limit: None,
|
||||||
|
start: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?
|
||||||
|
}
|
||||||
|
PaymentIdentifier::PaymentHash(payment_hash) => {
|
||||||
|
// Query by payment_hash
|
||||||
|
cln_client
|
||||||
|
.call_typed(&ListinvoicesRequest {
|
||||||
|
payment_hash: Some(hex::encode(payment_hash)),
|
||||||
label: None,
|
label: None,
|
||||||
invstring: None,
|
invstring: None,
|
||||||
offer_id: None,
|
offer_id: None,
|
||||||
@@ -359,31 +650,56 @@ impl MintPayment for Cln {
|
|||||||
start: None,
|
start: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(Error::from)?;
|
.map_err(Error::from)?
|
||||||
|
}
|
||||||
let status = match listinvoices_response.invoices.first() {
|
PaymentIdentifier::CustomId(_) => {
|
||||||
Some(invoice_response) => cln_invoice_status_to_mint_state(invoice_response.status),
|
tracing::error!("Unsupported payment id for CLN");
|
||||||
None => {
|
return Err(payment::Error::UnknownPaymentState);
|
||||||
tracing::info!(
|
}
|
||||||
"Check invoice called on unknown look up id: {}",
|
PaymentIdentifier::Bolt12PaymentHash(_) => {
|
||||||
payment_hash
|
tracing::error!("Unsupported payment id for CLN");
|
||||||
);
|
return Err(payment::Error::UnknownPaymentState);
|
||||||
return Err(Error::WrongClnResponse.into());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(status)
|
Ok(listinvoices_response
|
||||||
|
.invoices
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.status == ListinvoicesInvoicesStatus::PAID)
|
||||||
|
.filter(|p| p.amount_msat.is_some()) // Filter out invoices without an amount
|
||||||
|
.map(|p| WaitPaymentResponse {
|
||||||
|
payment_identifier: payment_identifier.clone(),
|
||||||
|
payment_amount: p
|
||||||
|
.amount_msat
|
||||||
|
// Safe to expect since we filtered for Some
|
||||||
|
.expect("We have filter out those without amounts")
|
||||||
|
.msat()
|
||||||
|
.into(),
|
||||||
|
unit: CurrencyUnit::Msat,
|
||||||
|
payment_id: p.payment_hash.to_string(),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
payment_hash: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
|
let payment_hash = match payment_identifier {
|
||||||
|
PaymentIdentifier::PaymentHash(hash) => hash,
|
||||||
|
PaymentIdentifier::Bolt12PaymentHash(hash) => hash,
|
||||||
|
_ => {
|
||||||
|
tracing::error!("Unsupported identifier to check outgoing payment for cln.");
|
||||||
|
return Err(payment::Error::UnknownPaymentState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let listpays_response = cln_client
|
let listpays_response = cln_client
|
||||||
.call_typed(&ListpaysRequest {
|
.call_typed(&ListpaysRequest {
|
||||||
payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?),
|
payment_hash: Some(*Sha256::from_bytes_ref(payment_hash)),
|
||||||
bolt11: None,
|
bolt11: None,
|
||||||
status: None,
|
status: None,
|
||||||
start: None,
|
start: None,
|
||||||
@@ -398,7 +714,7 @@ impl MintPayment for Cln {
|
|||||||
let status = cln_pays_status_to_mint_state(pays_response.status);
|
let status = cln_pays_status_to_mint_state(pays_response.status);
|
||||||
|
|
||||||
Ok(MakePaymentResponse {
|
Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: pays_response.payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: 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
|
||||||
@@ -408,7 +724,7 @@ impl MintPayment for Cln {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => Ok(MakePaymentResponse {
|
None => Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: None,
|
payment_proof: None,
|
||||||
status: MeltQuoteState::Unknown,
|
status: MeltQuoteState::Unknown,
|
||||||
total_spent: Amount::ZERO,
|
total_spent: Amount::ZERO,
|
||||||
@@ -419,9 +735,13 @@ impl MintPayment for Cln {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Cln {
|
impl Cln {
|
||||||
|
async fn cln_client(&self) -> Result<ClnRpc, Error> {
|
||||||
|
Ok(cln_rpc::ClnRpc::new(&self.rpc_socket).await?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get last pay index for cln
|
/// Get last pay index for cln
|
||||||
async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
|
async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
|
||||||
let mut cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
|
let mut cln_client = self.cln_client().await?;
|
||||||
let listinvoices_response = cln_client
|
let listinvoices_response = cln_client
|
||||||
.call_typed(&ListinvoicesRequest {
|
.call_typed(&ListinvoicesRequest {
|
||||||
index: None,
|
index: None,
|
||||||
@@ -440,13 +760,40 @@ impl Cln {
|
|||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode string
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
async fn decode_string(&self, string: String) -> Result<DecodeResponse, Error> {
|
||||||
|
let mut cln_client = self.cln_client().await?;
|
||||||
|
|
||||||
|
cln_client
|
||||||
|
.call_typed(&DecodeRequest { string })
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Could not fetch invoice for offer: {:?}", err);
|
||||||
|
Error::ClnRpc(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState {
|
/// Checks that outgoing payment is not already paid
|
||||||
match status {
|
#[instrument(skip(self))]
|
||||||
ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid,
|
async fn check_outgoing_unpaided(
|
||||||
ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid,
|
&self,
|
||||||
ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
|
payment_identifier: &PaymentIdentifier,
|
||||||
|
) -> Result<(), payment::Error> {
|
||||||
|
let pay_state = self.check_outgoing_payment(payment_identifier).await?;
|
||||||
|
|
||||||
|
match pay_state.status {
|
||||||
|
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => Ok(()),
|
||||||
|
MeltQuoteState::Paid => {
|
||||||
|
tracing::debug!("Melt attempted on invoice already paid");
|
||||||
|
Err(payment::Error::InvoiceAlreadyPaid)
|
||||||
|
}
|
||||||
|
MeltQuoteState::Pending => {
|
||||||
|
tracing::debug!("Melt attempted on invoice already pending");
|
||||||
|
Err(payment::Error::InvoicePaymentPending)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,23 +807,57 @@ fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState {
|
|||||||
|
|
||||||
async fn fetch_invoice_by_payment_hash(
|
async fn fetch_invoice_by_payment_hash(
|
||||||
cln_client: &mut cln_rpc::ClnRpc,
|
cln_client: &mut cln_rpc::ClnRpc,
|
||||||
payment_hash: &str,
|
payment_hash: &Hash,
|
||||||
) -> Result<Option<ListinvoicesInvoices>, Error> {
|
) -> Result<Option<ListinvoicesInvoices>, Error> {
|
||||||
match cln_client
|
tracing::debug!("Fetching invoice by payment hash: {}", payment_hash);
|
||||||
.call_typed(&ListinvoicesRequest {
|
|
||||||
payment_hash: Some(payment_hash.to_string()),
|
let payment_hash_str = payment_hash.to_string();
|
||||||
|
tracing::debug!("Payment hash string: {}", payment_hash_str);
|
||||||
|
|
||||||
|
let request = ListinvoicesRequest {
|
||||||
|
payment_hash: Some(payment_hash_str),
|
||||||
index: None,
|
index: None,
|
||||||
invstring: None,
|
invstring: None,
|
||||||
label: None,
|
label: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
offer_id: None,
|
offer_id: None,
|
||||||
start: None,
|
start: None,
|
||||||
})
|
};
|
||||||
.await
|
tracing::debug!("Created ListinvoicesRequest");
|
||||||
{
|
|
||||||
Ok(invoice_response) => Ok(invoice_response.invoices.first().cloned()),
|
match cln_client.call_typed(&request).await {
|
||||||
|
Ok(invoice_response) => {
|
||||||
|
let invoice_count = invoice_response.invoices.len();
|
||||||
|
tracing::debug!(
|
||||||
|
"Received {} invoices for payment hash {}",
|
||||||
|
invoice_count,
|
||||||
|
payment_hash
|
||||||
|
);
|
||||||
|
|
||||||
|
if invoice_count > 0 {
|
||||||
|
let first_invoice = invoice_response.invoices.first().cloned();
|
||||||
|
if let Some(invoice) = &first_invoice {
|
||||||
|
tracing::debug!("Found invoice with payment hash {}", payment_hash);
|
||||||
|
tracing::debug!(
|
||||||
|
"Invoice details - local_offer_id: {:?}, status: {:?}",
|
||||||
|
invoice.local_offer_id,
|
||||||
|
invoice.status
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("No invoice found with payment hash {}", payment_hash);
|
||||||
|
}
|
||||||
|
Ok(first_invoice)
|
||||||
|
} else {
|
||||||
|
tracing::warn!("No invoices returned for payment hash {}", payment_hash);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Error fetching invoice: {e}");
|
tracing::error!(
|
||||||
|
"Error fetching invoice by payment hash {}: {}",
|
||||||
|
payment_hash,
|
||||||
|
e
|
||||||
|
);
|
||||||
Err(Error::from(e))
|
Err(Error::from(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ cbor-diag.workspace = true
|
|||||||
ciborium.workspace = true
|
ciborium.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
lightning-invoice.workspace = true
|
lightning-invoice.workspace = true
|
||||||
|
lightning.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cashu::MintInfo;
|
use cashu::{Amount, MintInfo};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::common::QuoteTTL;
|
use crate::common::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, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
|
BlindSignature, CurrencyUnit, Id, MeltQuoteState, Proof, Proofs, PublicKey, State,
|
||||||
State,
|
|
||||||
};
|
};
|
||||||
|
use crate::payment::PaymentIdentifier;
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
mod auth;
|
mod auth;
|
||||||
@@ -67,13 +67,20 @@ pub trait QuotesTransaction<'a> {
|
|||||||
async fn get_mint_quote(&mut self, quote_id: &Uuid)
|
async fn get_mint_quote(&mut self, quote_id: &Uuid)
|
||||||
-> Result<Option<MintMintQuote>, Self::Err>;
|
-> Result<Option<MintMintQuote>, Self::Err>;
|
||||||
/// Add [`MintMintQuote`]
|
/// Add [`MintMintQuote`]
|
||||||
async fn add_or_replace_mint_quote(&mut self, quote: MintMintQuote) -> Result<(), Self::Err>;
|
async fn add_mint_quote(&mut self, quote: MintMintQuote) -> Result<(), Self::Err>;
|
||||||
/// Update state of [`MintMintQuote`]
|
/// Increment amount paid [`MintMintQuote`]
|
||||||
async fn update_mint_quote_state(
|
async fn increment_mint_quote_amount_paid(
|
||||||
&mut self,
|
&mut self,
|
||||||
quote_id: &Uuid,
|
quote_id: &Uuid,
|
||||||
state: MintQuoteState,
|
amount_paid: Amount,
|
||||||
) -> Result<MintQuoteState, Self::Err>;
|
payment_id: String,
|
||||||
|
) -> Result<Amount, Self::Err>;
|
||||||
|
/// Increment amount paid [`MintMintQuote`]
|
||||||
|
async fn increment_mint_quote_amount_issued(
|
||||||
|
&mut self,
|
||||||
|
quote_id: &Uuid,
|
||||||
|
amount_issued: Amount,
|
||||||
|
) -> Result<Amount, Self::Err>;
|
||||||
/// Remove [`MintMintQuote`]
|
/// Remove [`MintMintQuote`]
|
||||||
async fn remove_mint_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
|
async fn remove_mint_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
|
||||||
/// Get [`mint::MeltQuote`] and lock it for update in this transaction
|
/// Get [`mint::MeltQuote`] and lock it for update in this transaction
|
||||||
@@ -88,7 +95,7 @@ pub trait QuotesTransaction<'a> {
|
|||||||
async fn update_melt_quote_request_lookup_id(
|
async fn update_melt_quote_request_lookup_id(
|
||||||
&mut self,
|
&mut self,
|
||||||
quote_id: &Uuid,
|
quote_id: &Uuid,
|
||||||
new_request_lookup_id: &str,
|
new_request_lookup_id: &PaymentIdentifier,
|
||||||
) -> Result<(), Self::Err>;
|
) -> Result<(), Self::Err>;
|
||||||
|
|
||||||
/// Update [`mint::MeltQuote`] state
|
/// Update [`mint::MeltQuote`] state
|
||||||
@@ -98,6 +105,7 @@ pub trait QuotesTransaction<'a> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
quote_id: &Uuid,
|
quote_id: &Uuid,
|
||||||
new_state: MeltQuoteState,
|
new_state: MeltQuoteState,
|
||||||
|
payment_proof: Option<String>,
|
||||||
) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
|
) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>;
|
||||||
/// Remove [`mint::MeltQuote`]
|
/// Remove [`mint::MeltQuote`]
|
||||||
async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
|
async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>;
|
||||||
@@ -106,6 +114,12 @@ pub trait QuotesTransaction<'a> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
request: &str,
|
request: &str,
|
||||||
) -> Result<Option<MintMintQuote>, Self::Err>;
|
) -> Result<Option<MintMintQuote>, Self::Err>;
|
||||||
|
|
||||||
|
/// Get all [`MintMintQuote`]s
|
||||||
|
async fn get_mint_quote_by_request_lookup_id(
|
||||||
|
&mut self,
|
||||||
|
request_lookup_id: &PaymentIdentifier,
|
||||||
|
) -> Result<Option<MintMintQuote>, Self::Err>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mint Quote Database trait
|
/// Mint Quote Database trait
|
||||||
@@ -125,15 +139,10 @@ pub trait QuotesDatabase {
|
|||||||
/// Get all [`MintMintQuote`]s
|
/// Get all [`MintMintQuote`]s
|
||||||
async fn get_mint_quote_by_request_lookup_id(
|
async fn get_mint_quote_by_request_lookup_id(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
request_lookup_id: &PaymentIdentifier,
|
||||||
) -> Result<Option<MintMintQuote>, Self::Err>;
|
) -> Result<Option<MintMintQuote>, Self::Err>;
|
||||||
/// Get Mint Quotes
|
/// Get Mint Quotes
|
||||||
async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
|
async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
|
||||||
/// Get Mint Quotes with state
|
|
||||||
async fn get_mint_quotes_with_state(
|
|
||||||
&self,
|
|
||||||
state: MintQuoteState,
|
|
||||||
) -> Result<Vec<MintMintQuote>, Self::Err>;
|
|
||||||
/// Get [`mint::MeltQuote`]
|
/// Get [`mint::MeltQuote`]
|
||||||
async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err>;
|
async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err>;
|
||||||
/// Get all [`mint::MeltQuote`]s
|
/// Get all [`mint::MeltQuote`]s
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub enum Error {
|
|||||||
/// Duplicate entry
|
/// Duplicate entry
|
||||||
#[error("Duplicate entry")]
|
#[error("Duplicate entry")]
|
||||||
Duplicate,
|
Duplicate,
|
||||||
|
/// Amount overflow
|
||||||
|
#[error("Amount overflow")]
|
||||||
|
AmountOverflow,
|
||||||
|
|
||||||
/// DHKE error
|
/// DHKE error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|||||||
@@ -91,6 +91,24 @@ pub enum Error {
|
|||||||
/// Multi-Part Payment not supported for unit and method
|
/// Multi-Part Payment not supported for unit and method
|
||||||
#[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
|
#[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
|
||||||
AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
|
AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
|
||||||
|
/// Duplicate Payment id
|
||||||
|
#[error("Payment id seen for mint")]
|
||||||
|
DuplicatePaymentId,
|
||||||
|
/// Pubkey required
|
||||||
|
#[error("Pubkey required")]
|
||||||
|
PubkeyRequired,
|
||||||
|
/// Invalid payment method
|
||||||
|
#[error("Invalid payment method")]
|
||||||
|
InvalidPaymentMethod,
|
||||||
|
/// Amount undefined
|
||||||
|
#[error("Amount undefined")]
|
||||||
|
AmountUndefined,
|
||||||
|
/// Unsupported payment method
|
||||||
|
#[error("Payment method unsupported")]
|
||||||
|
UnsupportedPaymentMethod,
|
||||||
|
/// Could not parse bolt12
|
||||||
|
#[error("Could not parse bolt12")]
|
||||||
|
Bolt12parse,
|
||||||
|
|
||||||
/// Internal Error - Send error
|
/// Internal Error - Send error
|
||||||
#[error("Internal send error: {0}")]
|
#[error("Internal send error: {0}")]
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub mod common;
|
|||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
|
pub mod melt;
|
||||||
|
#[cfg(feature = "mint")]
|
||||||
pub mod mint;
|
pub mod mint;
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
pub mod payment;
|
pub mod payment;
|
||||||
|
|||||||
26
crates/cdk-common/src/melt.rs
Normal file
26
crates/cdk-common/src/melt.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//! Melt types
|
||||||
|
use cashu::{MeltQuoteBolt11Request, MeltQuoteBolt12Request};
|
||||||
|
|
||||||
|
/// Melt quote request enum for different types of quotes
|
||||||
|
///
|
||||||
|
/// This enum represents the different types of melt quote requests
|
||||||
|
/// that can be made, either BOLT11 or BOLT12.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum MeltQuoteRequest {
|
||||||
|
/// Lightning Network BOLT11 invoice request
|
||||||
|
Bolt11(MeltQuoteBolt11Request),
|
||||||
|
/// Lightning Network BOLT12 offer request
|
||||||
|
Bolt12(MeltQuoteBolt12Request),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MeltQuoteBolt11Request> for MeltQuoteRequest {
|
||||||
|
fn from(request: MeltQuoteBolt11Request) -> Self {
|
||||||
|
MeltQuoteRequest::Bolt11(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MeltQuoteBolt12Request> for MeltQuoteRequest {
|
||||||
|
fn from(request: MeltQuoteBolt12Request) -> Self {
|
||||||
|
MeltQuoteRequest::Bolt12(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
use bitcoin::bip32::DerivationPath;
|
use bitcoin::bip32::DerivationPath;
|
||||||
use cashu::util::unix_time;
|
use cashu::util::unix_time;
|
||||||
use cashu::{MeltQuoteBolt11Response, MintQuoteBolt11Response};
|
use cashu::{
|
||||||
|
Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
|
||||||
|
MintQuoteBolt12Response, PaymentMethod,
|
||||||
|
};
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::nuts::{MeltQuoteState, MintQuoteState};
|
use crate::nuts::{MeltQuoteState, MintQuoteState};
|
||||||
|
use crate::payment::PaymentIdentifier;
|
||||||
use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
|
use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
|
||||||
|
|
||||||
/// Mint Quote Info
|
/// Mint Quote Info
|
||||||
@@ -15,54 +21,209 @@ pub struct MintQuote {
|
|||||||
/// Quote id
|
/// Quote id
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
/// Amount of quote
|
/// Amount of quote
|
||||||
pub amount: Amount,
|
pub amount: Option<Amount>,
|
||||||
/// Unit of quote
|
/// Unit of quote
|
||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Quote payment request e.g. bolt11
|
/// Quote payment request e.g. bolt11
|
||||||
pub request: String,
|
pub request: String,
|
||||||
/// Quote state
|
|
||||||
pub state: MintQuoteState,
|
|
||||||
/// Expiration time of quote
|
/// Expiration time of quote
|
||||||
pub expiry: u64,
|
pub expiry: u64,
|
||||||
/// Value used by ln backend to look up state of request
|
/// Value used by ln backend to look up state of request
|
||||||
pub request_lookup_id: String,
|
pub request_lookup_id: PaymentIdentifier,
|
||||||
/// Pubkey
|
/// Pubkey
|
||||||
pub pubkey: Option<PublicKey>,
|
pub pubkey: Option<PublicKey>,
|
||||||
/// Unix time quote was created
|
/// Unix time quote was created
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub created_time: u64,
|
pub created_time: u64,
|
||||||
/// Unix time quote was paid
|
/// Amount paid
|
||||||
pub paid_time: Option<u64>,
|
#[serde(default)]
|
||||||
/// Unix time quote was issued
|
amount_paid: Amount,
|
||||||
pub issued_time: Option<u64>,
|
/// Amount issued
|
||||||
|
#[serde(default)]
|
||||||
|
amount_issued: Amount,
|
||||||
|
/// Payment of payment(s) that filled quote
|
||||||
|
#[serde(default)]
|
||||||
|
pub payments: Vec<IncomingPayment>,
|
||||||
|
/// Payment Method
|
||||||
|
#[serde(default)]
|
||||||
|
pub payment_method: PaymentMethod,
|
||||||
|
/// Payment of payment(s) that filled quote
|
||||||
|
#[serde(default)]
|
||||||
|
pub issuance: Vec<Issuance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MintQuote {
|
impl MintQuote {
|
||||||
/// Create new [`MintQuote`]
|
/// Create new [`MintQuote`]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
id: Option<Uuid>,
|
||||||
request: String,
|
request: String,
|
||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
amount: Amount,
|
amount: Option<Amount>,
|
||||||
expiry: u64,
|
expiry: u64,
|
||||||
request_lookup_id: String,
|
request_lookup_id: PaymentIdentifier,
|
||||||
pubkey: Option<PublicKey>,
|
pubkey: Option<PublicKey>,
|
||||||
|
amount_paid: Amount,
|
||||||
|
amount_issued: Amount,
|
||||||
|
payment_method: PaymentMethod,
|
||||||
|
created_time: u64,
|
||||||
|
payments: Vec<IncomingPayment>,
|
||||||
|
issuance: Vec<Issuance>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let id = Uuid::new_v4();
|
let id = id.unwrap_or(Uuid::new_v4());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
amount,
|
amount,
|
||||||
unit,
|
unit,
|
||||||
request,
|
request,
|
||||||
state: MintQuoteState::Unpaid,
|
|
||||||
expiry,
|
expiry,
|
||||||
request_lookup_id,
|
request_lookup_id,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_time: unix_time(),
|
created_time,
|
||||||
paid_time: None,
|
amount_paid,
|
||||||
issued_time: None,
|
amount_issued,
|
||||||
|
payment_method,
|
||||||
|
payments,
|
||||||
|
issuance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Increment the amount paid on the mint quote by a given amount
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn increment_amount_paid(
|
||||||
|
&mut self,
|
||||||
|
additional_amount: Amount,
|
||||||
|
) -> Result<Amount, crate::Error> {
|
||||||
|
self.amount_paid = self
|
||||||
|
.amount_paid
|
||||||
|
.checked_add(additional_amount)
|
||||||
|
.ok_or(crate::Error::AmountOverflow)?;
|
||||||
|
Ok(self.amount_paid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Amount paid
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn amount_paid(&self) -> Amount {
|
||||||
|
self.amount_paid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment the amount issued on the mint quote by a given amount
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn increment_amount_issued(
|
||||||
|
&mut self,
|
||||||
|
additional_amount: Amount,
|
||||||
|
) -> Result<Amount, crate::Error> {
|
||||||
|
self.amount_issued = self
|
||||||
|
.amount_issued
|
||||||
|
.checked_add(additional_amount)
|
||||||
|
.ok_or(crate::Error::AmountOverflow)?;
|
||||||
|
Ok(self.amount_issued)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Amount issued
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn amount_issued(&self) -> Amount {
|
||||||
|
self.amount_issued
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get state of mint quote
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn state(&self) -> MintQuoteState {
|
||||||
|
self.compute_quote_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Existing payment ids of a mint quote
|
||||||
|
pub fn payment_ids(&self) -> Vec<&String> {
|
||||||
|
self.payments.iter().map(|a| &a.payment_id).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a payment ID to the list of payment IDs
|
||||||
|
///
|
||||||
|
/// Returns an error if the payment ID is already in the list
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub fn add_payment(
|
||||||
|
&mut self,
|
||||||
|
amount: Amount,
|
||||||
|
payment_id: String,
|
||||||
|
time: u64,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
let payment_ids = self.payment_ids();
|
||||||
|
if payment_ids.contains(&&payment_id) {
|
||||||
|
return Err(crate::Error::DuplicatePaymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payment = IncomingPayment::new(amount, payment_id, time);
|
||||||
|
|
||||||
|
self.payments.push(payment);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute quote state
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
fn compute_quote_state(&self) -> MintQuoteState {
|
||||||
|
if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
|
||||||
|
return MintQuoteState::Unpaid;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.amount_paid.cmp(&self.amount_issued) {
|
||||||
|
std::cmp::Ordering::Less => {
|
||||||
|
// self.amount_paid is less than other (amount issued)
|
||||||
|
// Handle case where paid amount is insufficient
|
||||||
|
tracing::error!("We should not have issued more then has been paid");
|
||||||
|
MintQuoteState::Issued
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
// We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
|
||||||
|
// self.amount_paid equals other (amount issued)
|
||||||
|
// Handle case where paid amount exactly matches
|
||||||
|
MintQuoteState::Issued
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Greater => {
|
||||||
|
// self.amount_paid is greater than other (amount issued)
|
||||||
|
// Handle case where paid amount exceeds required amount
|
||||||
|
MintQuoteState::Paid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Payments
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct IncomingPayment {
|
||||||
|
/// Amount
|
||||||
|
pub amount: Amount,
|
||||||
|
/// Pyament unix time
|
||||||
|
pub time: u64,
|
||||||
|
/// Payment id
|
||||||
|
pub payment_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncomingPayment {
|
||||||
|
/// New [`IncomingPayment`]
|
||||||
|
pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
payment_id,
|
||||||
|
time,
|
||||||
|
amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informattion about issued quote
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Issuance {
|
||||||
|
/// Amount
|
||||||
|
pub amount: Amount,
|
||||||
|
/// Time
|
||||||
|
pub time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Issuance {
|
||||||
|
/// Create new [`Issuance`]
|
||||||
|
pub fn new(amount: Amount, time: u64) -> Self {
|
||||||
|
Self { amount, time }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Melt Quote Info
|
/// Melt Quote Info
|
||||||
@@ -75,7 +236,7 @@ pub struct MeltQuote {
|
|||||||
/// Quote amount
|
/// Quote amount
|
||||||
pub amount: Amount,
|
pub amount: Amount,
|
||||||
/// Quote Payment request e.g. bolt11
|
/// Quote Payment request e.g. bolt11
|
||||||
pub request: String,
|
pub request: MeltPaymentRequest,
|
||||||
/// Quote fee reserve
|
/// Quote fee reserve
|
||||||
pub fee_reserve: Amount,
|
pub fee_reserve: Amount,
|
||||||
/// Quote state
|
/// Quote state
|
||||||
@@ -85,28 +246,33 @@ pub struct MeltQuote {
|
|||||||
/// Payment preimage
|
/// Payment preimage
|
||||||
pub payment_preimage: Option<String>,
|
pub payment_preimage: Option<String>,
|
||||||
/// Value used by ln backend to look up state of request
|
/// Value used by ln backend to look up state of request
|
||||||
pub request_lookup_id: String,
|
pub request_lookup_id: PaymentIdentifier,
|
||||||
/// Msat to pay
|
/// Payment options
|
||||||
///
|
///
|
||||||
/// Used for an amountless invoice
|
/// Used for amountless invoices and MPP payments
|
||||||
pub msat_to_pay: Option<Amount>,
|
pub options: Option<MeltOptions>,
|
||||||
/// Unix time quote was created
|
/// Unix time quote was created
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub created_time: u64,
|
pub created_time: u64,
|
||||||
/// Unix time quote was paid
|
/// Unix time quote was paid
|
||||||
pub paid_time: Option<u64>,
|
pub paid_time: Option<u64>,
|
||||||
|
/// Payment method
|
||||||
|
#[serde(default)]
|
||||||
|
pub payment_method: PaymentMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MeltQuote {
|
impl MeltQuote {
|
||||||
/// Create new [`MeltQuote`]
|
/// Create new [`MeltQuote`]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
request: String,
|
request: MeltPaymentRequest,
|
||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
fee_reserve: Amount,
|
fee_reserve: Amount,
|
||||||
expiry: u64,
|
expiry: u64,
|
||||||
request_lookup_id: String,
|
request_lookup_id: PaymentIdentifier,
|
||||||
msat_to_pay: Option<Amount>,
|
options: Option<MeltOptions>,
|
||||||
|
payment_method: PaymentMethod,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
@@ -120,9 +286,10 @@ impl MeltQuote {
|
|||||||
expiry,
|
expiry,
|
||||||
payment_preimage: None,
|
payment_preimage: None,
|
||||||
request_lookup_id,
|
request_lookup_id,
|
||||||
msat_to_pay,
|
options,
|
||||||
created_time: unix_time(),
|
created_time: unix_time(),
|
||||||
paid_time: None,
|
paid_time: None,
|
||||||
|
payment_method,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,16 +340,51 @@ impl From<MintQuote> for MintQuoteBolt11Response<Uuid> {
|
|||||||
fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
|
fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
|
||||||
MintQuoteBolt11Response {
|
MintQuoteBolt11Response {
|
||||||
quote: mint_quote.id,
|
quote: mint_quote.id,
|
||||||
|
state: mint_quote.state(),
|
||||||
request: mint_quote.request,
|
request: mint_quote.request,
|
||||||
state: mint_quote.state,
|
|
||||||
expiry: Some(mint_quote.expiry),
|
expiry: Some(mint_quote.expiry),
|
||||||
pubkey: mint_quote.pubkey,
|
pubkey: mint_quote.pubkey,
|
||||||
amount: Some(mint_quote.amount),
|
amount: mint_quote.amount,
|
||||||
unit: Some(mint_quote.unit.clone()),
|
unit: Some(mint_quote.unit.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<MintQuote> for MintQuoteBolt11Response<String> {
|
||||||
|
fn from(quote: MintQuote) -> Self {
|
||||||
|
let quote: MintQuoteBolt11Response<Uuid> = quote.into();
|
||||||
|
|
||||||
|
quote.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<Uuid> {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
|
||||||
|
Ok(MintQuoteBolt12Response {
|
||||||
|
quote: mint_quote.id,
|
||||||
|
request: mint_quote.request,
|
||||||
|
expiry: Some(mint_quote.expiry),
|
||||||
|
amount_paid: mint_quote.amount_paid,
|
||||||
|
amount_issued: mint_quote.amount_issued,
|
||||||
|
pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
|
||||||
|
amount: mint_quote.amount,
|
||||||
|
unit: mint_quote.unit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
|
||||||
|
let quote: MintQuoteBolt12Response<Uuid> = quote.try_into()?;
|
||||||
|
|
||||||
|
Ok(quote.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
|
impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
|
||||||
fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
|
fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
|
||||||
MeltQuoteBolt11Response {
|
MeltQuoteBolt11Response {
|
||||||
@@ -212,8 +414,61 @@ impl From<MeltQuote> for MeltQuoteBolt11Response<Uuid> {
|
|||||||
expiry: melt_quote.expiry,
|
expiry: melt_quote.expiry,
|
||||||
payment_preimage: melt_quote.payment_preimage,
|
payment_preimage: melt_quote.payment_preimage,
|
||||||
change: None,
|
change: None,
|
||||||
request: Some(melt_quote.request.clone()),
|
request: Some(melt_quote.request.to_string()),
|
||||||
unit: Some(melt_quote.unit.clone()),
|
unit: Some(melt_quote.unit.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Payment request
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum MeltPaymentRequest {
|
||||||
|
/// Bolt11 Payment
|
||||||
|
Bolt11 {
|
||||||
|
/// Bolt11 invoice
|
||||||
|
bolt11: Bolt11Invoice,
|
||||||
|
},
|
||||||
|
/// Bolt12 Payment
|
||||||
|
Bolt12 {
|
||||||
|
/// Offer
|
||||||
|
#[serde(with = "offer_serde")]
|
||||||
|
offer: Box<Offer>,
|
||||||
|
/// Invoice
|
||||||
|
invoice: Option<Vec<u8>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MeltPaymentRequest {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
|
||||||
|
MeltPaymentRequest::Bolt12 { offer, invoice: _ } => write!(f, "{offer}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod offer_serde {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
use super::Offer;
|
||||||
|
|
||||||
|
pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s = offer.to_string();
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Ok(Box::new(Offer::from_str(&s).map_err(|_| {
|
||||||
|
serde::de::Error::custom("Invalid Bolt12 Offer")
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
//! CDK Mint Lightning
|
//! CDK Mint Lightning
|
||||||
|
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cashu::MeltOptions;
|
use cashu::util::hex;
|
||||||
|
use cashu::{Bolt11Invoice, MeltOptions};
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use lightning_invoice::ParseOrSemanticError;
|
use lightning_invoice::ParseOrSemanticError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
|
use crate::mint::MeltPaymentRequest;
|
||||||
use crate::{mint, Amount};
|
use crate::nuts::{CurrencyUnit, MeltQuoteState};
|
||||||
|
use crate::Amount;
|
||||||
|
|
||||||
/// CDK Lightning Error
|
/// CDK Lightning Error
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -31,6 +35,9 @@ pub enum Error {
|
|||||||
/// Payment state is unknown
|
/// Payment state is unknown
|
||||||
#[error("Payment state is unknown")]
|
#[error("Payment state is unknown")]
|
||||||
UnknownPaymentState,
|
UnknownPaymentState,
|
||||||
|
/// Amount mismatch
|
||||||
|
#[error("Amount is not what is expected")]
|
||||||
|
AmountMismatch,
|
||||||
/// Lightning Error
|
/// Lightning Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Lightning(Box<dyn std::error::Error + Send + Sync>),
|
Lightning(Box<dyn std::error::Error + Send + Sync>),
|
||||||
@@ -55,11 +62,186 @@ pub enum Error {
|
|||||||
/// NUT23 Error
|
/// NUT23 Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT23(#[from] crate::nuts::nut23::Error),
|
NUT23(#[from] crate::nuts::nut23::Error),
|
||||||
|
/// Hex error
|
||||||
|
#[error("Hex error")]
|
||||||
|
Hex(#[from] hex::Error),
|
||||||
|
/// Invalid hash
|
||||||
|
#[error("Invalid hash")]
|
||||||
|
InvalidHash,
|
||||||
/// Custom
|
/// Custom
|
||||||
#[error("`{0}`")]
|
#[error("`{0}`")]
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Infallible> for Error {
|
||||||
|
fn from(_: Infallible) -> Self {
|
||||||
|
unreachable!("Infallible cannot be constructed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payment identifier types
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "value")]
|
||||||
|
pub enum PaymentIdentifier {
|
||||||
|
/// Label identifier
|
||||||
|
Label(String),
|
||||||
|
/// Offer ID identifier
|
||||||
|
OfferId(String),
|
||||||
|
/// Payment hash identifier
|
||||||
|
PaymentHash([u8; 32]),
|
||||||
|
/// Bolt12 payment hash
|
||||||
|
Bolt12PaymentHash([u8; 32]),
|
||||||
|
/// Custom Payment ID
|
||||||
|
CustomId(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaymentIdentifier {
|
||||||
|
/// Create new [`PaymentIdentifier`]
|
||||||
|
pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
|
||||||
|
match kind.to_lowercase().as_str() {
|
||||||
|
"label" => Ok(Self::Label(identifier.to_string())),
|
||||||
|
"offer_id" => Ok(Self::OfferId(identifier.to_string())),
|
||||||
|
"payment_hash" => Ok(Self::PaymentHash(
|
||||||
|
hex::decode(identifier)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidHash)?,
|
||||||
|
)),
|
||||||
|
"bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
|
||||||
|
hex::decode(identifier)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidHash)?,
|
||||||
|
)),
|
||||||
|
"custom" => Ok(Self::CustomId(identifier.to_string())),
|
||||||
|
_ => Err(Error::UnsupportedPaymentOption),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payment id kind
|
||||||
|
pub fn kind(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Label(_) => "label".to_string(),
|
||||||
|
Self::OfferId(_) => "offer_id".to_string(),
|
||||||
|
Self::PaymentHash(_) => "payment_hash".to_string(),
|
||||||
|
Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
|
||||||
|
Self::CustomId(_) => "custom".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PaymentIdentifier {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Label(l) => write!(f, "{l}"),
|
||||||
|
Self::OfferId(o) => write!(f, "{o}"),
|
||||||
|
Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
|
||||||
|
Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
|
||||||
|
Self::CustomId(c) => write!(f, "{c}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for creating a BOLT11 incoming payment request
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Bolt11IncomingPaymentOptions {
|
||||||
|
/// Optional description for the payment request
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Amount for the payment request in sats
|
||||||
|
pub amount: Amount,
|
||||||
|
/// Optional expiry time as Unix timestamp in seconds
|
||||||
|
pub unix_expiry: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for creating a BOLT12 incoming payment request
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Bolt12IncomingPaymentOptions {
|
||||||
|
/// Optional description for the payment request
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Optional amount for the payment request in sats
|
||||||
|
pub amount: Option<Amount>,
|
||||||
|
/// Optional expiry time as Unix timestamp in seconds
|
||||||
|
pub unix_expiry: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for creating an incoming payment request
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum IncomingPaymentOptions {
|
||||||
|
/// BOLT11 payment request options
|
||||||
|
Bolt11(Bolt11IncomingPaymentOptions),
|
||||||
|
/// BOLT12 payment request options
|
||||||
|
Bolt12(Box<Bolt12IncomingPaymentOptions>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for BOLT11 outgoing payments
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Bolt11OutgoingPaymentOptions {
|
||||||
|
/// Bolt11
|
||||||
|
pub bolt11: Bolt11Invoice,
|
||||||
|
/// Maximum fee amount allowed for the payment
|
||||||
|
pub max_fee_amount: Option<Amount>,
|
||||||
|
/// Optional timeout in seconds
|
||||||
|
pub timeout_secs: Option<u64>,
|
||||||
|
/// Melt options
|
||||||
|
pub melt_options: Option<MeltOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for BOLT12 outgoing payments
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Bolt12OutgoingPaymentOptions {
|
||||||
|
/// Offer
|
||||||
|
pub offer: Offer,
|
||||||
|
/// Maximum fee amount allowed for the payment
|
||||||
|
pub max_fee_amount: Option<Amount>,
|
||||||
|
/// Optional timeout in seconds
|
||||||
|
pub timeout_secs: Option<u64>,
|
||||||
|
/// Bolt12 Invoice
|
||||||
|
pub invoice: Option<Vec<u8>>,
|
||||||
|
/// Melt options
|
||||||
|
pub melt_options: Option<MeltOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for creating an outgoing payment
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum OutgoingPaymentOptions {
|
||||||
|
/// BOLT11 payment options
|
||||||
|
Bolt11(Box<Bolt11OutgoingPaymentOptions>),
|
||||||
|
/// BOLT12 payment options
|
||||||
|
Bolt12(Box<Bolt12OutgoingPaymentOptions>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::Error> {
|
||||||
|
match melt_quote.request {
|
||||||
|
MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
|
||||||
|
Bolt11OutgoingPaymentOptions {
|
||||||
|
max_fee_amount: Some(melt_quote.fee_reserve),
|
||||||
|
timeout_secs: None,
|
||||||
|
bolt11,
|
||||||
|
melt_options: melt_quote.options,
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
MeltPaymentRequest::Bolt12 { offer, invoice } => {
|
||||||
|
let melt_options = match melt_quote.options {
|
||||||
|
None => None,
|
||||||
|
Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
|
||||||
|
Some(options) => Some(options),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(OutgoingPaymentOptions::Bolt12(Box::new(
|
||||||
|
Bolt12OutgoingPaymentOptions {
|
||||||
|
max_fee_amount: Some(melt_quote.fee_reserve),
|
||||||
|
timeout_secs: None,
|
||||||
|
offer: *offer,
|
||||||
|
invoice,
|
||||||
|
melt_options,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mint payment trait
|
/// Mint payment trait
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MintPayment {
|
pub trait MintPayment {
|
||||||
@@ -72,34 +254,30 @@ pub trait MintPayment {
|
|||||||
/// Create a new invoice
|
/// Create a new invoice
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
description: String,
|
options: IncomingPaymentOptions,
|
||||||
unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, 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,
|
||||||
request: &str,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
options: Option<MeltOptions>,
|
options: OutgoingPaymentOptions,
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err>;
|
) -> Result<PaymentQuoteResponse, Self::Err>;
|
||||||
|
|
||||||
/// Pay request
|
/// Pay request
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
unit: &CurrencyUnit,
|
||||||
partial_amount: Option<Amount>,
|
options: OutgoingPaymentOptions,
|
||||||
max_fee_amount: Option<Amount>,
|
|
||||||
) -> Result<MakePaymentResponse, 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_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err>;
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err>;
|
||||||
|
|
||||||
/// Is wait invoice active
|
/// Is wait invoice active
|
||||||
fn is_wait_invoice_active(&self) -> bool;
|
fn is_wait_invoice_active(&self) -> bool;
|
||||||
@@ -110,21 +288,36 @@ pub trait MintPayment {
|
|||||||
/// Check the status of an incoming payment
|
/// Check the status of an incoming payment
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err>;
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
|
||||||
|
|
||||||
/// Check the status of an outgoing payment
|
/// Check the status of an outgoing payment
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MakePaymentResponse, Self::Err>;
|
) -> Result<MakePaymentResponse, Self::Err>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait any invoice response
|
||||||
|
#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct WaitPaymentResponse {
|
||||||
|
/// Request look up id
|
||||||
|
/// Id that relates the quote and payment request
|
||||||
|
pub payment_identifier: PaymentIdentifier,
|
||||||
|
/// Payment amount
|
||||||
|
pub payment_amount: Amount,
|
||||||
|
/// Unit
|
||||||
|
pub unit: CurrencyUnit,
|
||||||
|
/// Unique id of payment
|
||||||
|
// Payment hash
|
||||||
|
pub payment_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Create incoming payment response
|
/// Create incoming payment response
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CreateIncomingPaymentResponse {
|
pub struct CreateIncomingPaymentResponse {
|
||||||
/// Id that is used to look up the payment 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: PaymentIdentifier,
|
||||||
/// Payment request
|
/// Payment request
|
||||||
pub request: String,
|
pub request: String,
|
||||||
/// Unix Expiry of Invoice
|
/// Unix Expiry of Invoice
|
||||||
@@ -135,7 +328,7 @@ pub struct CreateIncomingPaymentResponse {
|
|||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct MakePaymentResponse {
|
pub struct MakePaymentResponse {
|
||||||
/// Payment hash
|
/// Payment hash
|
||||||
pub payment_lookup_id: String,
|
pub payment_lookup_id: PaymentIdentifier,
|
||||||
/// Payment proof
|
/// Payment proof
|
||||||
pub payment_proof: Option<String>,
|
pub payment_proof: Option<String>,
|
||||||
/// Status
|
/// Status
|
||||||
@@ -150,7 +343,7 @@ pub struct MakePaymentResponse {
|
|||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct PaymentQuoteResponse {
|
pub struct PaymentQuoteResponse {
|
||||||
/// Request look up id
|
/// Request look up id
|
||||||
pub request_lookup_id: String,
|
pub request_lookup_id: PaymentIdentifier,
|
||||||
/// Amount
|
/// Amount
|
||||||
pub amount: Amount,
|
pub amount: Amount,
|
||||||
/// Fee required for melt
|
/// Fee required for melt
|
||||||
@@ -159,6 +352,18 @@ pub struct PaymentQuoteResponse {
|
|||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Status
|
/// Status
|
||||||
pub state: MeltQuoteState,
|
pub state: MeltQuoteState,
|
||||||
|
/// Payment Quote Options
|
||||||
|
pub options: Option<PaymentQuoteOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payment quote options
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PaymentQuoteOptions {
|
||||||
|
/// Bolt12 payment options
|
||||||
|
Bolt12 {
|
||||||
|
/// Bolt12 invoice
|
||||||
|
invoice: Option<Vec<u8>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ln backend settings
|
/// Ln backend settings
|
||||||
@@ -172,6 +377,8 @@ pub struct Bolt11Settings {
|
|||||||
pub invoice_description: bool,
|
pub invoice_description: bool,
|
||||||
/// Paying amountless invoices supported
|
/// Paying amountless invoices supported
|
||||||
pub amountless: bool,
|
pub amountless: bool,
|
||||||
|
/// Bolt12 supported
|
||||||
|
pub bolt12: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Bolt11Settings> for Value {
|
impl TryFrom<Bolt11Settings> for Value {
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ impl Indexable for NotificationPayload<Uuid> {
|
|||||||
NotificationPayload::MintQuoteBolt11Response(mint_quote) => {
|
NotificationPayload::MintQuoteBolt11Response(mint_quote) => {
|
||||||
vec![Index::from(Notification::MintQuoteBolt11(mint_quote.quote))]
|
vec![Index::from(Notification::MintQuoteBolt11(mint_quote.quote))]
|
||||||
}
|
}
|
||||||
|
NotificationPayload::MintQuoteBolt12Response(mint_quote) => {
|
||||||
|
vec![Index::from(Notification::MintQuoteBolt12(mint_quote.quote))]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||||
use cashu::util::hex;
|
use cashu::util::hex;
|
||||||
use cashu::{nut00, Proofs, PublicKey};
|
use cashu::{nut00, PaymentMethod, Proofs, PublicKey};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
@@ -42,8 +42,11 @@ pub struct MintQuote {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Mint Url
|
/// Mint Url
|
||||||
pub mint_url: MintUrl,
|
pub mint_url: MintUrl,
|
||||||
|
/// Payment method
|
||||||
|
#[serde(default)]
|
||||||
|
pub payment_method: PaymentMethod,
|
||||||
/// Amount of quote
|
/// Amount of quote
|
||||||
pub amount: Amount,
|
pub amount: Option<Amount>,
|
||||||
/// Unit of quote
|
/// Unit of quote
|
||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Quote payment request e.g. bolt11
|
/// Quote payment request e.g. bolt11
|
||||||
@@ -54,6 +57,12 @@ pub struct MintQuote {
|
|||||||
pub expiry: u64,
|
pub expiry: u64,
|
||||||
/// Secretkey for signing mint quotes [NUT-20]
|
/// Secretkey for signing mint quotes [NUT-20]
|
||||||
pub secret_key: Option<SecretKey>,
|
pub secret_key: Option<SecretKey>,
|
||||||
|
/// Amount minted
|
||||||
|
#[serde(default)]
|
||||||
|
pub amount_issued: Amount,
|
||||||
|
/// Amount paid to the mint for the quote
|
||||||
|
#[serde(default)]
|
||||||
|
pub amount_paid: Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Melt Quote Info
|
/// Melt Quote Info
|
||||||
@@ -77,6 +86,62 @@ pub struct MeltQuote {
|
|||||||
pub payment_preimage: Option<String>,
|
pub payment_preimage: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MintQuote {
|
||||||
|
/// Create a new MintQuote
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
payment_method: PaymentMethod,
|
||||||
|
amount: Option<Amount>,
|
||||||
|
unit: CurrencyUnit,
|
||||||
|
request: String,
|
||||||
|
expiry: u64,
|
||||||
|
secret_key: Option<SecretKey>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
mint_url,
|
||||||
|
payment_method,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
request,
|
||||||
|
state: MintQuoteState::Unpaid,
|
||||||
|
expiry,
|
||||||
|
secret_key,
|
||||||
|
amount_issued: Amount::ZERO,
|
||||||
|
amount_paid: Amount::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the total amount including any fees
|
||||||
|
pub fn total_amount(&self) -> Amount {
|
||||||
|
self.amount_paid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the quote has expired
|
||||||
|
pub fn is_expired(&self, current_time: u64) -> bool {
|
||||||
|
current_time > self.expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Amount that can be minted
|
||||||
|
pub fn amount_mintable(&self) -> Amount {
|
||||||
|
if self.amount_issued > self.amount_paid {
|
||||||
|
return Amount::ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
let difference = self.amount_paid - self.amount_issued;
|
||||||
|
|
||||||
|
if difference == Amount::ZERO && self.state != MintQuoteState::Issued {
|
||||||
|
if let Some(amount) = self.amount {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
difference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send Kind
|
/// Send Kind
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
pub enum SendKind {
|
pub enum SendKind {
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub fn notification_uuid_to_notification_string(
|
|||||||
NotificationPayload::MintQuoteBolt11Response(quote) => {
|
NotificationPayload::MintQuoteBolt11Response(quote) => {
|
||||||
NotificationPayload::MintQuoteBolt11Response(quote.to_string_id())
|
NotificationPayload::MintQuoteBolt11Response(quote.to_string_id())
|
||||||
}
|
}
|
||||||
|
NotificationPayload::MintQuoteBolt12Response(quote) => {
|
||||||
|
NotificationPayload::MintQuoteBolt12Response(quote.to_string_id())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ readme = "README.md"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bitcoin.workspace = true
|
bitcoin.workspace = true
|
||||||
cdk = { workspace = true, features = ["mint"] }
|
cdk-common = { workspace = true, features = ["mint"] }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
@@ -22,5 +22,6 @@ 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
|
||||||
|
lightning.workspace = true
|
||||||
tokio-stream.workspace = true
|
tokio-stream.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub enum Error {
|
|||||||
NoReceiver,
|
NoReceiver,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for cdk::cdk_payment::Error {
|
impl From<Error> for cdk_common::payment::Error {
|
||||||
fn from(e: Error) -> Self {
|
fn from(e: Error) -> Self {
|
||||||
Self::Lightning(Box::new(e))
|
Self::Lightning(Box::new(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
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;
|
||||||
|
|
||||||
@@ -17,22 +16,23 @@ 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::{to_unit, Amount};
|
use cdk_common::amount::{to_unit, Amount};
|
||||||
use cdk::cdk_payment::{
|
use cdk_common::common::FeeReserve;
|
||||||
self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
|
use cdk_common::ensure_cdk;
|
||||||
PaymentQuoteResponse,
|
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||||
|
use cdk_common::payment::{
|
||||||
|
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||||
|
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||||
|
PaymentQuoteResponse, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
|
|
||||||
use cdk::types::FeeReserve;
|
|
||||||
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::offers::offer::OfferBuilder;
|
||||||
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
|
use lightning_invoice::{Bolt11Invoice, Currency, InvoiceBuilder, PaymentSecret};
|
||||||
use reqwest::Client;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{Mutex, RwLock};
|
||||||
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;
|
||||||
@@ -44,13 +44,16 @@ pub mod error;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FakeWallet {
|
pub struct FakeWallet {
|
||||||
fee_reserve: FeeReserve,
|
fee_reserve: FeeReserve,
|
||||||
sender: tokio::sync::mpsc::Sender<String>,
|
#[allow(clippy::type_complexity)]
|
||||||
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<String>>>>,
|
sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount)>,
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<(PaymentIdentifier, Amount)>>>>,
|
||||||
payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
|
payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
|
||||||
failed_payment_check: Arc<Mutex<HashSet<String>>>,
|
failed_payment_check: Arc<Mutex<HashSet<String>>>,
|
||||||
payment_delay: u64,
|
payment_delay: u64,
|
||||||
wait_invoice_cancel_token: CancellationToken,
|
wait_invoice_cancel_token: CancellationToken,
|
||||||
wait_invoice_is_active: Arc<AtomicBool>,
|
wait_invoice_is_active: Arc<AtomicBool>,
|
||||||
|
incoming_payments: Arc<RwLock<HashMap<PaymentIdentifier, Vec<WaitPaymentResponse>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeWallet {
|
impl FakeWallet {
|
||||||
@@ -72,6 +75,7 @@ impl FakeWallet {
|
|||||||
payment_delay,
|
payment_delay,
|
||||||
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)),
|
||||||
|
incoming_payments: Arc::new(RwLock::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +106,7 @@ impl Default for FakeInvoiceDescription {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl MintPayment for FakeWallet {
|
impl MintPayment for FakeWallet {
|
||||||
type Err = cdk_payment::Error;
|
type Err = payment::Error;
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn get_settings(&self) -> Result<Value, Self::Err> {
|
async fn get_settings(&self) -> Result<Value, Self::Err> {
|
||||||
@@ -111,6 +115,7 @@ impl MintPayment for FakeWallet {
|
|||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
amountless: false,
|
amountless: false,
|
||||||
|
bolt12: false,
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,51 +132,86 @@ impl MintPayment for FakeWallet {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn wait_any_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||||
tracing::info!("Starting stream for fake invoices");
|
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)
|
||||||
|
.unwrap();
|
||||||
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(
|
||||||
|
|(request_lookup_id, payment_amount)| WaitPaymentResponse {
|
||||||
|
payment_identifier: request_lookup_id.clone(),
|
||||||
|
payment_amount,
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
payment_id: request_lookup_id.to_string(),
|
||||||
|
},
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn get_payment_quote(
|
async fn get_payment_quote(
|
||||||
&self,
|
&self,
|
||||||
request: &str,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
options: Option<MeltOptions>,
|
options: OutgoingPaymentOptions,
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||||
let bolt11 = Bolt11Invoice::from_str(request)?;
|
let (amount_msat, request_lookup_id) = match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
// If we have specific amount options, use those
|
||||||
|
let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
|
||||||
|
let msats = match melt_options {
|
||||||
|
MeltOptions::Amountless { amountless } => {
|
||||||
|
let amount_msat = amountless.amount_msat;
|
||||||
|
|
||||||
let amount_msat = match options {
|
if let Some(invoice_amount) =
|
||||||
Some(amount) => amount.amount_msat(),
|
bolt11_options.bolt11.amount_milli_satoshis()
|
||||||
None => bolt11
|
{
|
||||||
|
ensure_cdk!(
|
||||||
|
invoice_amount == u64::from(amount_msat),
|
||||||
|
Error::UnknownInvoiceAmount.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
amount_msat
|
||||||
|
}
|
||||||
|
MeltOptions::Mpp { mpp } => mpp.amount,
|
||||||
|
};
|
||||||
|
|
||||||
|
u64::from(msats)
|
||||||
|
} else {
|
||||||
|
// Fall back to invoice amount
|
||||||
|
bolt11_options
|
||||||
|
.bolt11
|
||||||
.amount_milli_satoshis()
|
.amount_milli_satoshis()
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
.into(),
|
|
||||||
};
|
};
|
||||||
|
let payment_id =
|
||||||
|
PaymentIdentifier::PaymentHash(*bolt11_options.bolt11.payment_hash().as_ref());
|
||||||
|
(amount_msat, payment_id)
|
||||||
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let offer = bolt12_options.offer;
|
||||||
|
|
||||||
let amount = if unit != &CurrencyUnit::Sat && unit != &CurrencyUnit::Msat {
|
let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
|
||||||
let client = Client::new();
|
amount.amount_msat().into()
|
||||||
|
|
||||||
let response: Value = client
|
|
||||||
.get("https://mempool.space/api/v1/prices")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::UnknownInvoice)?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let price = response.get(unit.to_string().to_uppercase()).unwrap();
|
|
||||||
|
|
||||||
let bitcoin_amount = u64::from(amount_msat) as f64 / 100_000_000_000.0;
|
|
||||||
let total_price = price.as_f64().unwrap() * bitcoin_amount;
|
|
||||||
|
|
||||||
Amount::from((total_price * 100.0).ceil() as u64)
|
|
||||||
} else {
|
} else {
|
||||||
to_unit(amount_msat, &CurrencyUnit::Msat, unit)?
|
// Fall back to offer amount
|
||||||
|
let amount = offer.amount().ok_or(Error::UnknownInvoiceAmount)?;
|
||||||
|
match amount {
|
||||||
|
lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
|
||||||
|
_ => return Err(Error::UnknownInvoiceAmount.into()),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
(
|
||||||
|
amount_msat,
|
||||||
|
PaymentIdentifier::OfferId(offer.id().to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -181,28 +221,30 @@ impl MintPayment for FakeWallet {
|
|||||||
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
||||||
|
|
||||||
Ok(PaymentQuoteResponse {
|
Ok(PaymentQuoteResponse {
|
||||||
request_lookup_id: bolt11.payment_hash().to_string(),
|
request_lookup_id,
|
||||||
amount,
|
amount,
|
||||||
fee: fee.into(),
|
fee: fee.into(),
|
||||||
unit: unit.clone(),
|
|
||||||
state: MeltQuoteState::Unpaid,
|
state: MeltQuoteState::Unpaid,
|
||||||
|
options: None,
|
||||||
|
unit: unit.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
unit: &CurrencyUnit,
|
||||||
_partial_msats: Option<Amount>,
|
options: OutgoingPaymentOptions,
|
||||||
_max_fee_msats: Option<Amount>,
|
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
|
match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let bolt11 = bolt11_options.bolt11;
|
||||||
let payment_hash = bolt11.payment_hash().to_string();
|
let payment_hash = bolt11.payment_hash().to_string();
|
||||||
|
|
||||||
let description = bolt11.description().to_string();
|
let description = bolt11.description().to_string();
|
||||||
|
|
||||||
let status: Option<FakeInvoiceDescription> = serde_json::from_str(&description).ok();
|
let status: Option<FakeInvoiceDescription> =
|
||||||
|
serde_json::from_str(&description).ok();
|
||||||
|
|
||||||
let mut payment_states = self.payment_states.lock().await;
|
let mut payment_states = self.payment_states.lock().await;
|
||||||
let payment_status = status
|
let payment_status = status
|
||||||
@@ -226,51 +268,151 @@ impl MintPayment for FakeWallet {
|
|||||||
ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
|
ensure_cdk!(!description.pay_err, Error::UnknownInvoice.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let amount_msat: u64 = if let Some(melt_options) = bolt11_options.melt_options {
|
||||||
|
melt_options.amount_msat().into()
|
||||||
|
} else {
|
||||||
|
// Fall back to invoice amount
|
||||||
|
bolt11
|
||||||
|
.amount_milli_satoshis()
|
||||||
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_spent = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
|
||||||
|
|
||||||
Ok(MakePaymentResponse {
|
Ok(MakePaymentResponse {
|
||||||
payment_proof: Some("".to_string()),
|
payment_proof: Some("".to_string()),
|
||||||
payment_lookup_id: payment_hash,
|
payment_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
*bolt11.payment_hash().as_ref(),
|
||||||
|
),
|
||||||
status: payment_status,
|
status: payment_status,
|
||||||
total_spent: melt_quote.amount + 1.into(),
|
total_spent: total_spent + 1.into(),
|
||||||
unit: melt_quote.unit,
|
unit: unit.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let bolt12 = bolt12_options.offer;
|
||||||
|
let amount_msat: u64 = if let Some(amount) = bolt12_options.melt_options {
|
||||||
|
amount.amount_msat().into()
|
||||||
|
} else {
|
||||||
|
// Fall back to offer amount
|
||||||
|
let amount = bolt12.amount().ok_or(Error::UnknownInvoiceAmount)?;
|
||||||
|
match amount {
|
||||||
|
lightning::offers::offer::Amount::Bitcoin { amount_msats } => amount_msats,
|
||||||
|
_ => return Err(Error::UnknownInvoiceAmount.into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_spent = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
|
||||||
|
|
||||||
|
Ok(MakePaymentResponse {
|
||||||
|
payment_proof: Some("".to_string()),
|
||||||
|
payment_lookup_id: PaymentIdentifier::OfferId(bolt12.id().to_string()),
|
||||||
|
status: MeltQuoteState::Paid,
|
||||||
|
total_spent: total_spent + 1.into(),
|
||||||
|
unit: unit.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
unit: &CurrencyUnit,
|
||||||
_unit: &CurrencyUnit,
|
options: IncomingPaymentOptions,
|
||||||
description: String,
|
|
||||||
_unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
||||||
|
let (payment_hash, request, amount, expiry) = match options {
|
||||||
|
IncomingPaymentOptions::Bolt12(bolt12_options) => {
|
||||||
|
let description = bolt12_options.description.unwrap_or_default();
|
||||||
|
let amount = bolt12_options.amount;
|
||||||
|
let expiry = bolt12_options.unix_expiry;
|
||||||
|
|
||||||
|
let secret_key = SecretKey::new(&mut thread_rng());
|
||||||
|
let secp_ctx = Secp256k1::new();
|
||||||
|
|
||||||
|
let offer_builder = OfferBuilder::new(secret_key.public_key(&secp_ctx))
|
||||||
|
.description(description.clone());
|
||||||
|
|
||||||
|
let offer_builder = match amount {
|
||||||
|
Some(amount) => {
|
||||||
|
let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
||||||
|
offer_builder.amount_msats(amount_msat.into())
|
||||||
|
}
|
||||||
|
None => offer_builder,
|
||||||
|
};
|
||||||
|
|
||||||
|
let offer = offer_builder.build().unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
PaymentIdentifier::OfferId(offer.id().to_string()),
|
||||||
|
offer.to_string(),
|
||||||
|
amount.unwrap_or(Amount::ZERO),
|
||||||
|
expiry,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IncomingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let description = bolt11_options.description.unwrap_or_default();
|
||||||
|
let amount = bolt11_options.amount;
|
||||||
|
let expiry = bolt11_options.unix_expiry;
|
||||||
|
|
||||||
// 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 invoice = create_fake_invoice(amount.into(), description.clone());
|
||||||
|
|
||||||
let invoice = create_fake_invoice(amount_msat.into(), description);
|
|
||||||
|
|
||||||
let sender = self.sender.clone();
|
|
||||||
|
|
||||||
let payment_hash = invoice.payment_hash();
|
let payment_hash = invoice.payment_hash();
|
||||||
|
|
||||||
let payment_hash_clone = payment_hash.to_string();
|
(
|
||||||
|
PaymentIdentifier::PaymentHash(*payment_hash.as_ref()),
|
||||||
|
invoice.to_string(),
|
||||||
|
amount,
|
||||||
|
expiry,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sender = self.sender.clone();
|
||||||
let duration = time::Duration::from_secs(self.payment_delay);
|
let duration = time::Duration::from_secs(self.payment_delay);
|
||||||
|
|
||||||
|
let final_amount = if amount == Amount::ZERO {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
// Generate a random number between 1 and 1000 (inclusive)
|
||||||
|
let random_number: u64 = rng.gen_range(1..=1000);
|
||||||
|
random_number.into()
|
||||||
|
} else {
|
||||||
|
amount
|
||||||
|
};
|
||||||
|
|
||||||
|
let payment_hash_clone = payment_hash.clone();
|
||||||
|
|
||||||
|
let incoming_payment = self.incoming_payments.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Wait for the random delay to elapse
|
// Wait for the random delay to elapse
|
||||||
time::sleep(duration).await;
|
time::sleep(duration).await;
|
||||||
|
|
||||||
|
let response = WaitPaymentResponse {
|
||||||
|
payment_identifier: payment_hash_clone.clone(),
|
||||||
|
payment_amount: final_amount,
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
payment_id: payment_hash_clone.to_string(),
|
||||||
|
};
|
||||||
|
let mut incoming = incoming_payment.write().await;
|
||||||
|
incoming
|
||||||
|
.entry(payment_hash_clone.clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(response.clone());
|
||||||
|
|
||||||
// Send the message after waiting for the specified duration
|
// Send the message after waiting for the specified duration
|
||||||
if sender.send(payment_hash_clone.clone()).await.is_err() {
|
if sender
|
||||||
tracing::error!("Failed to send label: {}", payment_hash_clone);
|
.send((payment_hash_clone.clone(), final_amount))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to send label: {:?}", payment_hash_clone);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let expiry = invoice.expires_at().map(|t| t.as_secs());
|
|
||||||
|
|
||||||
Ok(CreateIncomingPaymentResponse {
|
Ok(CreateIncomingPaymentResponse {
|
||||||
request_lookup_id: payment_hash.to_string(),
|
request_lookup_id: payment_hash,
|
||||||
request: invoice.to_string(),
|
request,
|
||||||
expiry,
|
expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -278,31 +420,37 @@ impl MintPayment for FakeWallet {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
_request_lookup_id: &str,
|
request_lookup_id: &PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err> {
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
|
||||||
Ok(MintQuoteState::Paid)
|
Ok(self
|
||||||
|
.incoming_payments
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get(request_lookup_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(vec![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
request_lookup_id: &PaymentIdentifier,
|
||||||
) -> Result<MakePaymentResponse, 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.to_string()).cloned();
|
||||||
|
|
||||||
let status = status.unwrap_or(MeltQuoteState::Paid);
|
let status = status.unwrap_or(MeltQuoteState::Paid);
|
||||||
|
|
||||||
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.to_string()) {
|
||||||
return Err(cdk_payment::Error::InvoicePaymentPending);
|
return Err(payment::Error::InvoicePaymentPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(MakePaymentResponse {
|
Ok(MakePaymentResponse {
|
||||||
payment_proof: Some("".to_string()),
|
payment_proof: Some("".to_string()),
|
||||||
payment_lookup_id: request_lookup_id.to_string(),
|
payment_lookup_id: request_lookup_id.clone(),
|
||||||
status,
|
status,
|
||||||
total_spent: Amount::ZERO,
|
total_spent: Amount::ZERO,
|
||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::{env, fs};
|
|||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
|
use cashu::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
use cdk::cdk_database::{self, WalletDatabase};
|
use cdk::cdk_database::{self, WalletDatabase};
|
||||||
use cdk::mint::{MintBuilder, MintMeltLimits};
|
use cdk::mint::{MintBuilder, MintMeltLimits};
|
||||||
@@ -72,7 +73,7 @@ impl MintConnector for DirectMintConnection {
|
|||||||
request: MintQuoteBolt11Request,
|
request: MintQuoteBolt11Request,
|
||||||
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
||||||
self.mint
|
self.mint
|
||||||
.get_mint_bolt11_quote(request)
|
.get_mint_quote(request.into())
|
||||||
.await
|
.await
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ impl MintConnector for DirectMintConnection {
|
|||||||
request: MeltQuoteBolt11Request,
|
request: MeltQuoteBolt11Request,
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
self.mint
|
self.mint
|
||||||
.get_melt_bolt11_quote(&request)
|
.get_melt_quote(request.into())
|
||||||
.await
|
.await
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
@@ -119,7 +120,7 @@ impl MintConnector for DirectMintConnection {
|
|||||||
request: MeltRequest<String>,
|
request: MeltRequest<String>,
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
let request_uuid = request.try_into().unwrap();
|
let request_uuid = request.try_into().unwrap();
|
||||||
self.mint.melt_bolt11(&request_uuid).await.map(Into::into)
|
self.mint.melt(&request_uuid).await.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
|
async fn post_swap(&self, swap_request: SwapRequest) -> Result<SwapResponse, Error> {
|
||||||
@@ -152,6 +153,59 @@ impl MintConnector for DirectMintConnection {
|
|||||||
|
|
||||||
*auth_wallet = wallet;
|
*auth_wallet = wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn post_mint_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MintQuoteBolt12Request,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error> {
|
||||||
|
let res: MintQuoteBolt12Response<Uuid> =
|
||||||
|
self.mint.get_mint_quote(request.into()).await?.try_into()?;
|
||||||
|
Ok(res.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_mint_quote_bolt12_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error> {
|
||||||
|
let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
|
||||||
|
let quote: MintQuoteBolt12Response<Uuid> = self
|
||||||
|
.mint
|
||||||
|
.check_mint_quote("e_id_uuid)
|
||||||
|
.await?
|
||||||
|
.try_into()?;
|
||||||
|
|
||||||
|
Ok(quote.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Quote [NUT-23]
|
||||||
|
async fn post_melt_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MeltQuoteBolt12Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
self.mint
|
||||||
|
.get_melt_quote(request.into())
|
||||||
|
.await
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
/// Melt Quote Status [NUT-23]
|
||||||
|
async fn get_melt_bolt12_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let quote_id_uuid = Uuid::from_str(quote_id).unwrap();
|
||||||
|
self.mint
|
||||||
|
.check_melt_quote("e_id_uuid)
|
||||||
|
.await
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
/// Melt [NUT-23]
|
||||||
|
async fn post_melt_bolt12(
|
||||||
|
&self,
|
||||||
|
_request: MeltRequest<String>,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
// Implementation to be added later
|
||||||
|
Err(Error::UnsupportedPaymentMethod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_tracing() {
|
pub fn setup_tracing() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::env;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use cashu::Bolt11Invoice;
|
use cashu::{Bolt11Invoice, PaymentMethod};
|
||||||
use cdk::amount::{Amount, SplitTarget};
|
use cdk::amount::{Amount, SplitTarget};
|
||||||
use cdk::nuts::{MintQuoteState, NotificationPayload, State};
|
use cdk::nuts::{MintQuoteState, NotificationPayload, State};
|
||||||
use cdk::wallet::WalletSubscription;
|
use cdk::wallet::WalletSubscription;
|
||||||
@@ -86,6 +86,10 @@ pub async fn wait_for_mint_to_be_paid(
|
|||||||
if response.state == MintQuoteState::Paid {
|
if response.state == MintQuoteState::Paid {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
} else if let NotificationPayload::MintQuoteBolt12Response(response) = msg {
|
||||||
|
if response.amount_paid > Amount::ZERO {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(anyhow!("Subscription ended without quote being paid"))
|
Err(anyhow!("Subscription ended without quote being paid"))
|
||||||
@@ -95,9 +99,17 @@ pub async fn wait_for_mint_to_be_paid(
|
|||||||
|
|
||||||
let check_interval = Duration::from_secs(5);
|
let check_interval = Duration::from_secs(5);
|
||||||
|
|
||||||
|
let method = wallet
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(mint_quote_id)
|
||||||
|
.await?
|
||||||
|
.map(|q| q.payment_method)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let periodic_task = async {
|
let periodic_task = async {
|
||||||
loop {
|
loop {
|
||||||
match wallet.mint_quote_state(mint_quote_id).await {
|
match method {
|
||||||
|
PaymentMethod::Bolt11 => match wallet.mint_quote_state(mint_quote_id).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if result.state == MintQuoteState::Paid {
|
if result.state == MintQuoteState::Paid {
|
||||||
tracing::info!("mint quote paid via poll");
|
tracing::info!("mint quote paid via poll");
|
||||||
@@ -107,6 +119,20 @@ pub async fn wait_for_mint_to_be_paid(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Could not check mint quote status: {:?}", e);
|
tracing::error!("Could not check mint quote status: {:?}", e);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
PaymentMethod::Bolt12 => {
|
||||||
|
match wallet.mint_bolt12_quote_state(mint_quote_id).await {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.amount_paid > Amount::ZERO {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Could not check mint quote status: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PaymentMethod::Custom(_) => (),
|
||||||
}
|
}
|
||||||
sleep(check_interval).await;
|
sleep(check_interval).await;
|
||||||
}
|
}
|
||||||
@@ -166,7 +192,6 @@ pub async fn init_lnd_client() -> LndClient {
|
|||||||
pub async fn pay_if_regtest(invoice: &Bolt11Invoice) -> Result<()> {
|
pub async fn pay_if_regtest(invoice: &Bolt11Invoice) -> Result<()> {
|
||||||
// Check if the invoice is for the regtest network
|
// Check if the invoice is for the regtest network
|
||||||
if invoice.network() == bitcoin::Network::Regtest {
|
if invoice.network() == bitcoin::Network::Regtest {
|
||||||
println!("Regtest invoice");
|
|
||||||
let lnd_client = init_lnd_client().await;
|
let lnd_client = init_lnd_client().await;
|
||||||
lnd_client.pay_invoice(invoice.to_string()).await?;
|
lnd_client.pay_invoice(invoice.to_string()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
332
crates/cdk-integration-tests/tests/bolt12.rs
Normal file
332
crates/cdk-integration-tests/tests/bolt12.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use bip39::Mnemonic;
|
||||||
|
use cashu::amount::SplitTarget;
|
||||||
|
use cashu::nut23::Amountless;
|
||||||
|
use cashu::{Amount, CurrencyUnit, MintRequest, PreMintSecrets, ProofsMethods};
|
||||||
|
use cdk::wallet::{HttpClient, MintConnector, Wallet};
|
||||||
|
use cdk_integration_tests::init_regtest::get_cln_dir;
|
||||||
|
use cdk_integration_tests::{get_mint_url_from_env, wait_for_mint_to_be_paid};
|
||||||
|
use cdk_sqlite::wallet::memory;
|
||||||
|
use ln_regtest_rs::ln_client::ClnClient;
|
||||||
|
|
||||||
|
/// Tests basic BOLT12 minting functionality:
|
||||||
|
/// - Creates a wallet
|
||||||
|
/// - Gets a BOLT12 quote for a specific amount (100 sats)
|
||||||
|
/// - Pays the quote using Core Lightning
|
||||||
|
/// - Mints tokens and verifies the correct amount is received
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_bolt12_mint() {
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await.unwrap()),
|
||||||
|
&Mnemonic::generate(12).unwrap().to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mint_amount = Amount::from(100);
|
||||||
|
|
||||||
|
let mint_quote = wallet
|
||||||
|
.mint_bolt12_quote(Some(mint_amount), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||||
|
|
||||||
|
let cln_one_dir = get_cln_dir("one");
|
||||||
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await.unwrap();
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(None, mint_quote.request)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(proofs.total_amount().unwrap(), 100.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests multiple payments to a single BOLT12 quote:
|
||||||
|
/// - Creates a wallet and gets a BOLT12 quote without specifying amount
|
||||||
|
/// - Makes two separate payments (10,000 sats and 11,000 sats) to the same quote
|
||||||
|
/// - Verifies that each payment can be minted separately and correctly
|
||||||
|
/// - Tests the functionality of reusing a quote for multiple payments
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_bolt12_mint_multiple() -> Result<()> {
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await?),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
|
||||||
|
|
||||||
|
let cln_one_dir = get_cln_dir("one");
|
||||||
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(Some(10000), mint_quote.request.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||||
|
|
||||||
|
wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(proofs.total_amount().unwrap(), 10.into());
|
||||||
|
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(Some(11_000), mint_quote.request)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||||
|
|
||||||
|
wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(proofs.total_amount().unwrap(), 11.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that multiple wallets can pay the same BOLT12 offer:
|
||||||
|
/// - Creates a BOLT12 offer through CLN that both wallets will pay
|
||||||
|
/// - Creates two separate wallets with different minting amounts
|
||||||
|
/// - Has each wallet get their own quote and make payments
|
||||||
|
/// - Verifies both wallets can successfully mint their tokens
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_bolt12_multiple_wallets() -> Result<()> {
|
||||||
|
// Create first wallet
|
||||||
|
let wallet_one = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await?),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create second wallet
|
||||||
|
let wallet_two = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await?),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create a BOLT12 offer that both wallets will use
|
||||||
|
let cln_one_dir = get_cln_dir("one");
|
||||||
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
||||||
|
// First wallet payment
|
||||||
|
let quote_one = wallet_one
|
||||||
|
.mint_bolt12_quote(Some(10_000.into()), None)
|
||||||
|
.await?;
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(None, quote_one.request.clone())
|
||||||
|
.await?;
|
||||||
|
wait_for_mint_to_be_paid(&wallet_one, "e_one.id, 60).await?;
|
||||||
|
let proofs_one = wallet_one
|
||||||
|
.mint_bolt12("e_one.id, None, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(proofs_one.total_amount()?, 10_000.into());
|
||||||
|
|
||||||
|
// Second wallet payment
|
||||||
|
let quote_two = wallet_two
|
||||||
|
.mint_bolt12_quote(Some(15_000.into()), None)
|
||||||
|
.await?;
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(None, quote_two.request.clone())
|
||||||
|
.await?;
|
||||||
|
wait_for_mint_to_be_paid(&wallet_two, "e_two.id, 60).await?;
|
||||||
|
|
||||||
|
let proofs_two = wallet_two
|
||||||
|
.mint_bolt12("e_two.id, None, SplitTarget::default(), None)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(proofs_two.total_amount()?, 15_000.into());
|
||||||
|
|
||||||
|
let offer = cln_client
|
||||||
|
.get_bolt12_offer(None, false, "test_multiple_wallets".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let wallet_one_melt_quote = wallet_one
|
||||||
|
.melt_bolt12_quote(
|
||||||
|
offer.to_string(),
|
||||||
|
Some(cashu::MeltOptions::Amountless {
|
||||||
|
amountless: Amountless {
|
||||||
|
amount_msat: 1500.into(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let wallet_two_melt_quote = wallet_two
|
||||||
|
.melt_bolt12_quote(
|
||||||
|
offer.to_string(),
|
||||||
|
Some(cashu::MeltOptions::Amountless {
|
||||||
|
amountless: Amountless {
|
||||||
|
amount_msat: 1000.into(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let melted = wallet_one.melt(&wallet_one_melt_quote.id).await?;
|
||||||
|
|
||||||
|
assert!(melted.preimage.is_some());
|
||||||
|
|
||||||
|
let melted_two = wallet_two.melt(&wallet_two_melt_quote.id).await?;
|
||||||
|
|
||||||
|
assert!(melted_two.preimage.is_some());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests the BOLT12 melting (spending) functionality:
|
||||||
|
/// - Creates a wallet and mints 20,000 sats using BOLT12
|
||||||
|
/// - Creates a BOLT12 offer for 10,000 sats
|
||||||
|
/// - Tests melting (spending) tokens using the BOLT12 offer
|
||||||
|
/// - Verifies the correct amount is melted
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_bolt12_melt() -> Result<()> {
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await?),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wallet.get_mint_info().await?;
|
||||||
|
|
||||||
|
let mint_amount = Amount::from(20_000);
|
||||||
|
|
||||||
|
// Create a single-use BOLT12 quote
|
||||||
|
let mint_quote = wallet.mint_bolt12_quote(Some(mint_amount), None).await?;
|
||||||
|
|
||||||
|
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||||
|
// Pay the quote
|
||||||
|
let cln_one_dir = get_cln_dir("one");
|
||||||
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(None, mint_quote.request.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Wait for payment to be processed
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||||
|
|
||||||
|
let offer = cln_client
|
||||||
|
.get_bolt12_offer(Some(10_000), true, "hhhhhhhh".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let _proofs = wallet
|
||||||
|
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let quote = wallet.melt_bolt12_quote(offer.to_string(), None).await?;
|
||||||
|
|
||||||
|
let melt = wallet.melt("e.id).await?;
|
||||||
|
|
||||||
|
assert_eq!(melt.amount, 10.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests security validation for BOLT12 minting to prevent overspending:
|
||||||
|
/// - Creates a wallet and gets an open-ended BOLT12 quote
|
||||||
|
/// - Makes a payment of 10,000 millisats
|
||||||
|
/// - Attempts to mint more tokens (500 sats) than were actually paid for
|
||||||
|
/// - Verifies that the mint correctly rejects the oversized mint request
|
||||||
|
/// - Ensures proper error handling with TransactionUnbalanced error
|
||||||
|
/// This test is crucial for ensuring the economic security of the minting process
|
||||||
|
/// by preventing users from minting more tokens than they have paid for.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_regtest_bolt12_mint_extra() -> Result<()> {
|
||||||
|
let wallet = Wallet::new(
|
||||||
|
&get_mint_url_from_env(),
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
Arc::new(memory::empty().await?),
|
||||||
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wallet.get_mint_info().await?;
|
||||||
|
|
||||||
|
// Create a single-use BOLT12 quote
|
||||||
|
let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
|
||||||
|
|
||||||
|
let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
|
||||||
|
|
||||||
|
assert_eq!(state.amount_paid, Amount::ZERO);
|
||||||
|
assert_eq!(state.amount_issued, Amount::ZERO);
|
||||||
|
|
||||||
|
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
|
let pay_amount_msats = 10_000;
|
||||||
|
|
||||||
|
let cln_one_dir = get_cln_dir("one");
|
||||||
|
let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?;
|
||||||
|
cln_client
|
||||||
|
.pay_bolt12_offer(Some(pay_amount_msats), mint_quote.request.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10).await?;
|
||||||
|
|
||||||
|
let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
|
||||||
|
|
||||||
|
assert_eq!(state.amount_paid, (pay_amount_msats / 1_000).into());
|
||||||
|
assert_eq!(state.amount_issued, Amount::ZERO);
|
||||||
|
|
||||||
|
let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?;
|
||||||
|
|
||||||
|
let quote_info = wallet
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(&mint_quote.id)
|
||||||
|
.await?
|
||||||
|
.expect("there is a quote");
|
||||||
|
|
||||||
|
let mut mint_request = MintRequest {
|
||||||
|
quote: mint_quote.id,
|
||||||
|
outputs: pre_mint.blinded_messages(),
|
||||||
|
signature: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(secret_key) = quote_info.secret_key {
|
||||||
|
mint_request.sign(secret_key)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
|
||||||
|
|
||||||
|
let response = http_client.post_mint(mint_request.clone()).await;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Err(err) => match err {
|
||||||
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
||||||
|
err => {
|
||||||
|
bail!("Wrong mint error returned: {}", err.to_string());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(_) => {
|
||||||
|
bail!("Should not have allowed second payment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -514,7 +514,7 @@ async fn test_reuse_auth_proof() {
|
|||||||
.await
|
.await
|
||||||
.expect("Quote should be allowed");
|
.expect("Quote should be allowed");
|
||||||
|
|
||||||
assert!(quote.amount == 10.into());
|
assert!(quote.amount == Some(10.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet
|
wallet
|
||||||
@@ -645,7 +645,7 @@ async fn test_refresh_access_token() {
|
|||||||
.await
|
.await
|
||||||
.expect("failed to get mint quote with refreshed token");
|
.expect("failed to get mint quote with refreshed token");
|
||||||
|
|
||||||
assert_eq!(mint_quote.amount, mint_amount);
|
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||||
|
|
||||||
// Verify the total number of auth tokens
|
// Verify the total number of auth tokens
|
||||||
let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
|
let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
@@ -731,7 +731,7 @@ async fn test_auth_token_spending_order() {
|
|||||||
.await
|
.await
|
||||||
.expect("failed to get mint quote");
|
.expect("failed to get mint quote");
|
||||||
|
|
||||||
assert_eq!(mint_quote.amount, 10.into());
|
assert_eq!(mint_quote.amount, Some(10.into()));
|
||||||
|
|
||||||
// Check remaining tokens after each operation
|
// Check remaining tokens after each operation
|
||||||
let remaining = wallet.get_unspent_auth_proofs().await.unwrap();
|
let remaining = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ async fn test_happy_mint_melt_round_trip() {
|
|||||||
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
||||||
pay_if_regtest(&invoice).await.unwrap();
|
pay_if_regtest(&invoice).await.unwrap();
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let proofs = wallet
|
let proofs = wallet
|
||||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
.await
|
.await
|
||||||
@@ -210,7 +214,7 @@ async fn test_happy_mint() {
|
|||||||
|
|
||||||
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(mint_quote.amount, mint_amount);
|
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||||
|
|
||||||
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
||||||
pay_if_regtest(&invoice).await.unwrap();
|
pay_if_regtest(&invoice).await.unwrap();
|
||||||
@@ -285,6 +289,8 @@ async fn test_restore() {
|
|||||||
let restored = wallet_2.restore().await.unwrap();
|
let restored = wallet_2.restore().await.unwrap();
|
||||||
let proofs = wallet_2.get_unspent_proofs().await.unwrap();
|
let proofs = wallet_2.get_unspent_proofs().await.unwrap();
|
||||||
|
|
||||||
|
assert!(!proofs.is_empty());
|
||||||
|
|
||||||
let expected_fee = wallet.get_proofs_fee(&proofs).await.unwrap();
|
let expected_fee = wallet.get_proofs_fee(&proofs).await.unwrap();
|
||||||
wallet_2
|
wallet_2
|
||||||
.swap(None, SplitTarget::default(), proofs, None, false)
|
.swap(None, SplitTarget::default(), proofs, None, false)
|
||||||
@@ -431,8 +437,10 @@ async fn test_pay_invoice_twice() {
|
|||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
cdk::Error::RequestAlreadyPaid => (),
|
cdk::Error::RequestAlreadyPaid => (),
|
||||||
err => {
|
err => {
|
||||||
|
if !err.to_string().contains("Duplicate entry") {
|
||||||
panic!("Wrong invoice already paid: {}", err.to_string());
|
panic!("Wrong invoice already paid: {}", err.to_string());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
panic!("Should not have allowed second payment");
|
panic!("Should not have allowed second payment");
|
||||||
|
|||||||
@@ -832,11 +832,11 @@ async fn test_concurrent_double_spend_melt() {
|
|||||||
let melt_request3 = melt_request.clone();
|
let melt_request3 = melt_request.clone();
|
||||||
|
|
||||||
// Spawn 3 concurrent tasks to process the melt requests
|
// Spawn 3 concurrent tasks to process the melt requests
|
||||||
let task1 = tokio::spawn(async move { mint_clone1.melt_bolt11(&melt_request).await });
|
let task1 = tokio::spawn(async move { mint_clone1.melt(&melt_request).await });
|
||||||
|
|
||||||
let task2 = tokio::spawn(async move { mint_clone2.melt_bolt11(&melt_request2).await });
|
let task2 = tokio::spawn(async move { mint_clone2.melt(&melt_request2).await });
|
||||||
|
|
||||||
let task3 = tokio::spawn(async move { mint_clone3.melt_bolt11(&melt_request3).await });
|
let task3 = tokio::spawn(async move { mint_clone3.melt(&melt_request3).await });
|
||||||
|
|
||||||
// Wait for all tasks to complete
|
// Wait for all tasks to complete
|
||||||
let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
|
let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ async fn test_internal_payment() {
|
|||||||
|
|
||||||
let _melted = wallet.melt(&melt.id).await.unwrap();
|
let _melted = wallet.melt(&melt.id).await.unwrap();
|
||||||
|
|
||||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
|
wait_for_mint_to_be_paid(&wallet_2, &mint_quote.id, 60)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ async fn test_regtest_melt_amountless() {
|
|||||||
|
|
||||||
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(mint_quote.amount, mint_amount);
|
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||||
|
|
||||||
lnd_client
|
lnd_client
|
||||||
.pay_invoice(mint_quote.request)
|
.pay_invoice(mint_quote.request)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ async fn test_swap() {
|
|||||||
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
||||||
pay_if_regtest(&invoice).await.unwrap();
|
pay_if_regtest(&invoice).await.unwrap();
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 10)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let _mint_amount = wallet
|
let _mint_amount = wallet
|
||||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ pub enum Error {
|
|||||||
/// Amount overflow
|
/// Amount overflow
|
||||||
#[error("Amount overflow")]
|
#[error("Amount overflow")]
|
||||||
AmountOverflow,
|
AmountOverflow,
|
||||||
|
/// Invalid payment hash
|
||||||
|
#[error("Invalid payment hash")]
|
||||||
|
InvalidPaymentHash,
|
||||||
/// Anyhow error
|
/// Anyhow error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Anyhow(#[from] anyhow::Error),
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
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;
|
||||||
|
|
||||||
@@ -15,13 +14,14 @@ use async_trait::async_trait;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
||||||
use cdk_common::common::FeeReserve;
|
use cdk_common::common::FeeReserve;
|
||||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
|
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||||
use cdk_common::payment::{
|
use cdk_common::payment::{
|
||||||
self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
|
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||||
PaymentQuoteResponse,
|
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||||
|
PaymentQuoteResponse, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk_common::util::unix_time;
|
use cdk_common::util::{hex, unix_time};
|
||||||
use cdk_common::{mint, Bolt11Invoice};
|
use cdk_common::Bolt11Invoice;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use lnbits_rs::api::invoice::CreateInvoiceRequest;
|
use lnbits_rs::api::invoice::CreateInvoiceRequest;
|
||||||
@@ -65,6 +65,7 @@ impl LNbits {
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
amountless: false,
|
amountless: false,
|
||||||
|
bolt12: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ impl MintPayment for LNbits {
|
|||||||
|
|
||||||
async fn wait_any_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||||
let api = self.lnbits_api.clone();
|
let api = self.lnbits_api.clone();
|
||||||
let cancel_token = self.wait_invoice_cancel_token.clone();
|
let cancel_token = self.wait_invoice_cancel_token.clone();
|
||||||
let is_active = Arc::clone(&self.wait_invoice_is_active);
|
let is_active = Arc::clone(&self.wait_invoice_is_active);
|
||||||
@@ -122,23 +123,45 @@ impl MintPayment for LNbits {
|
|||||||
msg_option = receiver.recv() => {
|
msg_option = receiver.recv() => {
|
||||||
match msg_option {
|
match msg_option {
|
||||||
Some(msg) => {
|
Some(msg) => {
|
||||||
let check = api.is_invoice_paid(&msg).await;
|
let check = api.get_payment_info(&msg).await;
|
||||||
|
|
||||||
match check {
|
match check {
|
||||||
Ok(state) => {
|
Ok(payment) => {
|
||||||
if state {
|
if payment.paid {
|
||||||
Some((msg, (api, cancel_token, is_active)))
|
match hex::decode(msg.clone()) {
|
||||||
|
Ok(decoded) => {
|
||||||
|
match decoded.try_into() {
|
||||||
|
Ok(hash) => {
|
||||||
|
let response = WaitPaymentResponse {
|
||||||
|
payment_identifier: PaymentIdentifier::PaymentHash(hash),
|
||||||
|
payment_amount: Amount::from(payment.details.amount as u64),
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
payment_id: msg.clone()
|
||||||
|
};
|
||||||
|
Some((response, (api, cancel_token, is_active)))
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to convert payment hash bytes to array: {:?}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to decode payment hash hex string: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Some(("".to_string(), (api, cancel_token, is_active)))
|
tracing::warn!("Received payment notification but could not check payment for {}", msg);
|
||||||
}
|
None
|
||||||
}
|
|
||||||
_ => Some(("".to_string(), (api, cancel_token, is_active))),
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None
|
||||||
}
|
}
|
||||||
|
},
|
||||||
None => {
|
None => {
|
||||||
is_active.store(false, Ordering::SeqCst);
|
is_active.store(false, Ordering::SeqCst);
|
||||||
None
|
None
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,24 +171,24 @@ impl MintPayment for LNbits {
|
|||||||
|
|
||||||
async fn get_payment_quote(
|
async fn get_payment_quote(
|
||||||
&self,
|
&self,
|
||||||
request: &str,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
options: Option<MeltOptions>,
|
options: OutgoingPaymentOptions,
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
) -> Result<PaymentQuoteResponse, 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 bolt11 = Bolt11Invoice::from_str(request)?;
|
match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
let amount_msat = match options {
|
let amount_msat = match bolt11_options.melt_options {
|
||||||
Some(amount) => {
|
Some(amount) => {
|
||||||
if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
|
if matches!(amount, MeltOptions::Mpp { mpp: _ }) {
|
||||||
return Err(payment::Error::UnsupportedPaymentOption);
|
return Err(payment::Error::UnsupportedPaymentOption);
|
||||||
}
|
}
|
||||||
amount.amount_msat()
|
amount.amount_msat()
|
||||||
}
|
}
|
||||||
None => bolt11
|
None => bolt11_options
|
||||||
|
.bolt11
|
||||||
.amount_milli_satoshis()
|
.amount_milli_satoshis()
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
.into(),
|
.into(),
|
||||||
@@ -181,23 +204,32 @@ impl MintPayment for LNbits {
|
|||||||
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
||||||
|
|
||||||
Ok(PaymentQuoteResponse {
|
Ok(PaymentQuoteResponse {
|
||||||
request_lookup_id: bolt11.payment_hash().to_string(),
|
request_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
*bolt11_options.bolt11.payment_hash().as_ref(),
|
||||||
|
),
|
||||||
amount,
|
amount,
|
||||||
unit: unit.clone(),
|
|
||||||
fee: fee.into(),
|
fee: fee.into(),
|
||||||
state: MeltQuoteState::Unpaid,
|
state: MeltQuoteState::Unpaid,
|
||||||
|
options: None,
|
||||||
|
unit: unit.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(_bolt12_options) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
_unit: &CurrencyUnit,
|
||||||
_partial_msats: Option<Amount>,
|
options: OutgoingPaymentOptions,
|
||||||
_max_fee_msats: Option<Amount>,
|
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
|
match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
let pay_response = self
|
let pay_response = self
|
||||||
.lnbits_api
|
.lnbits_api
|
||||||
.pay_invoice(&melt_quote.request, None)
|
.pay_invoice(&bolt11_options.bolt11.to_string(), None)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not pay invoice");
|
tracing::error!("Could not pay invoice");
|
||||||
@@ -215,9 +247,10 @@ impl MintPayment for LNbits {
|
|||||||
Self::Err::Anyhow(anyhow!("Could not find invoice"))
|
Self::Err::Anyhow(anyhow!("Could not find invoice"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let status = match invoice_info.paid {
|
let status = if invoice_info.paid {
|
||||||
true => MeltQuoteState::Paid,
|
MeltQuoteState::Unpaid
|
||||||
false => MeltQuoteState::Unpaid,
|
} else {
|
||||||
|
MeltQuoteState::Paid
|
||||||
};
|
};
|
||||||
|
|
||||||
let total_spent = Amount::from(
|
let total_spent = Amount::from(
|
||||||
@@ -230,27 +263,40 @@ impl MintPayment for LNbits {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Ok(MakePaymentResponse {
|
Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: pay_response.payment_hash,
|
payment_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
payment_proof: invoice_info.details.preimage,
|
hex::decode(pay_response.payment_hash)
|
||||||
|
.map_err(|_| Error::InvalidPaymentHash)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidPaymentHash)?,
|
||||||
|
),
|
||||||
|
payment_proof: Some(invoice_info.details.payment_hash),
|
||||||
status,
|
status,
|
||||||
total_spent,
|
total_spent,
|
||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(_) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
description: String,
|
options: IncomingPaymentOptions,
|
||||||
unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, 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();
|
match options {
|
||||||
|
IncomingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let description = bolt11_options.description.unwrap_or_default();
|
||||||
|
let amount = bolt11_options.amount;
|
||||||
|
let unix_expiry = bolt11_options.unix_expiry;
|
||||||
|
|
||||||
|
let time_now = unix_time();
|
||||||
let expiry = unix_expiry.map(|t| t - time_now);
|
let expiry = unix_expiry.map(|t| t - time_now);
|
||||||
|
|
||||||
let invoice_request = CreateInvoiceRequest {
|
let invoice_request = CreateInvoiceRequest {
|
||||||
@@ -280,19 +326,26 @@ impl MintPayment for LNbits {
|
|||||||
let expiry = request.expires_at().map(|t| t.as_secs());
|
let expiry = request.expires_at().map(|t| t.as_secs());
|
||||||
|
|
||||||
Ok(CreateIncomingPaymentResponse {
|
Ok(CreateIncomingPaymentResponse {
|
||||||
request_lookup_id: create_invoice_response.payment_hash().to_string(),
|
request_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
*request.payment_hash().as_ref(),
|
||||||
|
),
|
||||||
request: request.to_string(),
|
request: request.to_string(),
|
||||||
expiry,
|
expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
IncomingPaymentOptions::Bolt12(_) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LNbits")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
payment_hash: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err> {
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
|
||||||
let paid = self
|
let payment = self
|
||||||
.lnbits_api
|
.lnbits_api
|
||||||
.is_invoice_paid(payment_hash)
|
.get_payment_info(&payment_identifier.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not check invoice status");
|
tracing::error!("Could not check invoice status");
|
||||||
@@ -300,21 +353,21 @@ impl MintPayment for LNbits {
|
|||||||
Self::Err::Anyhow(anyhow!("Could not check invoice status"))
|
Self::Err::Anyhow(anyhow!("Could not check invoice status"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let state = match paid {
|
Ok(vec![WaitPaymentResponse {
|
||||||
true => MintQuoteState::Paid,
|
payment_identifier: payment_identifier.clone(),
|
||||||
false => MintQuoteState::Unpaid,
|
payment_amount: Amount::from(payment.details.amount as u64),
|
||||||
};
|
unit: CurrencyUnit::Sat,
|
||||||
|
payment_id: payment.details.payment_hash,
|
||||||
Ok(state)
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
payment_hash: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MakePaymentResponse, 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_identifier.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not check invoice status");
|
tracing::error!("Could not check invoice status");
|
||||||
@@ -323,7 +376,7 @@ impl MintPayment for LNbits {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let pay_response = MakePaymentResponse {
|
let pay_response = MakePaymentResponse {
|
||||||
payment_lookup_id: payment.details.payment_hash,
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: payment.preimage,
|
payment_proof: 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(
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ use async_trait::async_trait;
|
|||||||
use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
|
||||||
use cdk_common::bitcoin::hashes::Hash;
|
use cdk_common::bitcoin::hashes::Hash;
|
||||||
use cdk_common::common::FeeReserve;
|
use cdk_common::common::FeeReserve;
|
||||||
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
|
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
|
||||||
use cdk_common::payment::{
|
use cdk_common::payment::{
|
||||||
self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
|
self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
|
||||||
PaymentQuoteResponse,
|
MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
|
||||||
|
PaymentQuoteResponse, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk_common::util::hex;
|
use cdk_common::util::hex;
|
||||||
use cdk_common::{mint, Bolt11Invoice};
|
use cdk_common::Bolt11Invoice;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use lnrpc::fee_limit::Limit;
|
use lnrpc::fee_limit::Limit;
|
||||||
@@ -39,6 +40,8 @@ pub mod error;
|
|||||||
mod proto;
|
mod proto;
|
||||||
pub(crate) use proto::{lnrpc, routerrpc};
|
pub(crate) use proto::{lnrpc, routerrpc};
|
||||||
|
|
||||||
|
use crate::lnrpc::invoice::InvoiceState;
|
||||||
|
|
||||||
/// Lnd mint backend
|
/// Lnd mint backend
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Lnd {
|
pub struct Lnd {
|
||||||
@@ -108,6 +111,7 @@ impl Lnd {
|
|||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
amountless: true,
|
amountless: true,
|
||||||
|
bolt12: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -135,7 +139,7 @@ impl MintPayment for Lnd {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn wait_any_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||||
let mut lnd_client = self.lnd_client.clone();
|
let mut lnd_client = self.lnd_client.clone();
|
||||||
|
|
||||||
let stream_req = lnrpc::InvoiceSubscription {
|
let stream_req = lnrpc::InvoiceSubscription {
|
||||||
@@ -176,8 +180,23 @@ impl MintPayment for Lnd {
|
|||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
if msg.state == 1 {
|
if msg.state() == InvoiceState::Settled {
|
||||||
Some((hex::encode(msg.r_hash), (stream, cancel_token, is_active)))
|
|
||||||
|
let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
|
||||||
|
|
||||||
|
if let Ok(hash_slice) = hash_slice {
|
||||||
|
let hash = hex::encode(hash_slice);
|
||||||
|
|
||||||
|
tracing::info!("LND: Processing payment with hash: {}", hash);
|
||||||
|
let wait_response = WaitPaymentResponse {
|
||||||
|
payment_identifier: PaymentIdentifier::PaymentHash(hash_slice), payment_amount: Amount::from(msg.amt_paid_msat as u64),
|
||||||
|
unit: CurrencyUnit::Msat,
|
||||||
|
payment_id: hash,
|
||||||
|
};
|
||||||
|
tracing::info!("LND: Created WaitPaymentResponse with amount {} msat",
|
||||||
|
msg.amt_paid_msat);
|
||||||
|
Some((wait_response, (stream, cancel_token, is_active)))
|
||||||
|
} else { None }
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -205,15 +224,15 @@ impl MintPayment for Lnd {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn get_payment_quote(
|
async fn get_payment_quote(
|
||||||
&self,
|
&self,
|
||||||
request: &str,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
options: Option<MeltOptions>,
|
options: OutgoingPaymentOptions,
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||||
let bolt11 = Bolt11Invoice::from_str(request)?;
|
match options {
|
||||||
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
let amount_msat = match options {
|
let amount_msat = match bolt11_options.melt_options {
|
||||||
Some(amount) => amount.amount_msat(),
|
Some(amount) => amount.amount_msat(),
|
||||||
None => bolt11
|
None => bolt11_options
|
||||||
|
.bolt11
|
||||||
.amount_milli_satoshis()
|
.amount_milli_satoshis()
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
.ok_or(Error::UnknownInvoiceAmount)?
|
||||||
.into(),
|
.into(),
|
||||||
@@ -229,26 +248,36 @@ impl MintPayment for Lnd {
|
|||||||
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
let fee = max(relative_fee_reserve, absolute_fee_reserve);
|
||||||
|
|
||||||
Ok(PaymentQuoteResponse {
|
Ok(PaymentQuoteResponse {
|
||||||
request_lookup_id: bolt11.payment_hash().to_string(),
|
request_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
*bolt11_options.bolt11.payment_hash().as_ref(),
|
||||||
|
),
|
||||||
amount,
|
amount,
|
||||||
unit: unit.clone(),
|
|
||||||
fee: fee.into(),
|
fee: fee.into(),
|
||||||
state: MeltQuoteState::Unpaid,
|
state: MeltQuoteState::Unpaid,
|
||||||
|
options: None,
|
||||||
|
unit: unit.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(_) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
_unit: &CurrencyUnit,
|
||||||
partial_amount: Option<Amount>,
|
options: OutgoingPaymentOptions,
|
||||||
max_fee: Option<Amount>,
|
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
let payment_request = melt_quote.request;
|
match options {
|
||||||
let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
|
OutgoingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let bolt11 = bolt11_options.bolt11;
|
||||||
|
|
||||||
let pay_state = self
|
let pay_state = self
|
||||||
.check_outgoing_payment(&bolt11.payment_hash().to_string())
|
.check_outgoing_payment(&PaymentIdentifier::PaymentHash(
|
||||||
|
*bolt11.payment_hash().as_ref(),
|
||||||
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match pay_state.status {
|
match pay_state.status {
|
||||||
@@ -263,20 +292,16 @@ impl MintPayment for Lnd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
|
|
||||||
let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
|
|
||||||
Some(amount_msat) => amount_msat,
|
|
||||||
None => melt_quote
|
|
||||||
.msat_to_pay
|
|
||||||
.ok_or(Error::UnknownInvoiceAmount)?
|
|
||||||
.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Detect partial payments
|
// Detect partial payments
|
||||||
match partial_amount {
|
match bolt11_options.melt_options {
|
||||||
Some(part_amt) => {
|
Some(MeltOptions::Mpp { mpp }) => {
|
||||||
let partial_amount_msat = to_unit(part_amt, &melt_quote.unit, &CurrencyUnit::Msat)?;
|
let amount_msat: u64 = bolt11
|
||||||
let invoice = Bolt11Invoice::from_str(&payment_request)?;
|
.amount_milli_satoshis()
|
||||||
|
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||||
|
{
|
||||||
|
let partial_amount_msat = mpp.amount;
|
||||||
|
let invoice = bolt11;
|
||||||
|
let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
|
||||||
|
|
||||||
// Extract information from invoice
|
// Extract information from invoice
|
||||||
let pub_key = invoice.get_payee_pub_key();
|
let pub_key = invoice.get_payee_pub_key();
|
||||||
@@ -357,7 +382,9 @@ impl MintPayment for Lnd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Ok(MakePaymentResponse {
|
return Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: hex::encode(payment_hash),
|
payment_lookup_id: PaymentIdentifier::PaymentHash(
|
||||||
|
payment_hash.to_byte_array(),
|
||||||
|
),
|
||||||
payment_proof: payment_preimage,
|
payment_proof: payment_preimage,
|
||||||
status,
|
status,
|
||||||
total_spent: total_amt.into(),
|
total_spent: total_amt.into(),
|
||||||
@@ -370,11 +397,21 @@ impl MintPayment for Lnd {
|
|||||||
tracing::error!("Limit of retries reached, payment couldn't succeed.");
|
tracing::error!("Limit of retries reached, payment couldn't succeed.");
|
||||||
Err(Error::PaymentFailed.into())
|
Err(Error::PaymentFailed.into())
|
||||||
}
|
}
|
||||||
None => {
|
}
|
||||||
|
_ => {
|
||||||
let mut lnd_client = self.lnd_client.clone();
|
let mut lnd_client = self.lnd_client.clone();
|
||||||
|
|
||||||
|
let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
|
||||||
|
|
||||||
|
let amount_msat = u64::from(
|
||||||
|
bolt11_options
|
||||||
|
.melt_options
|
||||||
|
.map(|a| a.amount_msat())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
let pay_req = lnrpc::SendRequest {
|
let pay_req = lnrpc::SendRequest {
|
||||||
payment_request,
|
payment_request: bolt11.to_string(),
|
||||||
fee_limit: max_fee.map(|f| {
|
fee_limit: max_fee.map(|f| {
|
||||||
let limit = Limit::Fixed(u64::from(f) as i64);
|
let limit = Limit::Fixed(u64::from(f) as i64);
|
||||||
FeeLimit { limit: Some(limit) }
|
FeeLimit { limit: Some(limit) }
|
||||||
@@ -406,8 +443,11 @@ impl MintPayment for Lnd {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let payment_identifier =
|
||||||
|
PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
|
||||||
|
|
||||||
Ok(MakePaymentResponse {
|
Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: hex::encode(payment_response.payment_hash),
|
payment_lookup_id: payment_identifier,
|
||||||
payment_proof: payment_preimage,
|
payment_proof: payment_preimage,
|
||||||
status,
|
status,
|
||||||
total_spent: total_amount.into(),
|
total_spent: total_amount.into(),
|
||||||
@@ -416,19 +456,28 @@ impl MintPayment for Lnd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OutgoingPaymentOptions::Bolt12(_) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, description))]
|
#[instrument(skip(self, options))]
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
|
||||||
unit: &CurrencyUnit,
|
unit: &CurrencyUnit,
|
||||||
description: String,
|
options: IncomingPaymentOptions,
|
||||||
unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
||||||
let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
match options {
|
||||||
|
IncomingPaymentOptions::Bolt11(bolt11_options) => {
|
||||||
|
let description = bolt11_options.description.unwrap_or_default();
|
||||||
|
let amount = bolt11_options.amount;
|
||||||
|
let unix_expiry = bolt11_options.unix_expiry;
|
||||||
|
|
||||||
|
let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
|
||||||
|
|
||||||
let invoice_request = lnrpc::Invoice {
|
let invoice_request = lnrpc::Invoice {
|
||||||
value_msat: u64::from(amount) as i64,
|
value_msat: u64::from(amount_msat) as i64,
|
||||||
memo: description,
|
memo: description,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -444,22 +493,30 @@ impl MintPayment for Lnd {
|
|||||||
|
|
||||||
let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
|
let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
|
||||||
|
|
||||||
|
let payment_identifier =
|
||||||
|
PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
|
||||||
|
|
||||||
Ok(CreateIncomingPaymentResponse {
|
Ok(CreateIncomingPaymentResponse {
|
||||||
request_lookup_id: bolt11.payment_hash().to_string(),
|
request_lookup_id: payment_identifier,
|
||||||
request: bolt11.to_string(),
|
request: bolt11.to_string(),
|
||||||
expiry: unix_expiry,
|
expiry: unix_expiry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
IncomingPaymentOptions::Bolt12(_) => {
|
||||||
|
Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err> {
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
|
||||||
let mut lnd_client = self.lnd_client.clone();
|
let mut lnd_client = self.lnd_client.clone();
|
||||||
|
|
||||||
let invoice_request = lnrpc::PaymentHash {
|
let invoice_request = lnrpc::PaymentHash {
|
||||||
r_hash: hex::decode(request_lookup_id).unwrap(),
|
r_hash: hex::decode(payment_identifier.to_string()).unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -470,26 +527,27 @@ impl MintPayment for Lnd {
|
|||||||
.map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
|
.map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
match invoice.state {
|
if invoice.state() == InvoiceState::Settled {
|
||||||
// Open
|
Ok(vec![WaitPaymentResponse {
|
||||||
0 => Ok(MintQuoteState::Unpaid),
|
payment_identifier: payment_identifier.clone(),
|
||||||
// Settled
|
payment_amount: Amount::from(invoice.amt_paid_msat as u64),
|
||||||
1 => Ok(MintQuoteState::Paid),
|
unit: CurrencyUnit::Msat,
|
||||||
// Canceled
|
payment_id: hex::encode(invoice.r_hash),
|
||||||
2 => Ok(MintQuoteState::Unpaid),
|
}])
|
||||||
// Accepted
|
} else {
|
||||||
3 => Ok(MintQuoteState::Unpaid),
|
Ok(vec![])
|
||||||
_ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
payment_hash: &str,
|
payment_identifier: &PaymentIdentifier,
|
||||||
) -> Result<MakePaymentResponse, Self::Err> {
|
) -> Result<MakePaymentResponse, Self::Err> {
|
||||||
let mut lnd_client = self.lnd_client.clone();
|
let mut lnd_client = self.lnd_client.clone();
|
||||||
|
|
||||||
|
let payment_hash = &payment_identifier.to_string();
|
||||||
|
|
||||||
let track_request = routerrpc::TrackPaymentRequest {
|
let track_request = 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,
|
||||||
@@ -503,7 +561,7 @@ impl MintPayment for Lnd {
|
|||||||
let err_code = err.code();
|
let err_code = err.code();
|
||||||
if err_code == tonic::Code::NotFound {
|
if err_code == tonic::Code::NotFound {
|
||||||
return Ok(MakePaymentResponse {
|
return Ok(MakePaymentResponse {
|
||||||
payment_lookup_id: payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: None,
|
payment_proof: None,
|
||||||
status: MeltQuoteState::Unknown,
|
status: MeltQuoteState::Unknown,
|
||||||
total_spent: Amount::ZERO,
|
total_spent: Amount::ZERO,
|
||||||
@@ -522,7 +580,7 @@ impl MintPayment for Lnd {
|
|||||||
|
|
||||||
let response = match status {
|
let response = match status {
|
||||||
PaymentStatus::Unknown => MakePaymentResponse {
|
PaymentStatus::Unknown => MakePaymentResponse {
|
||||||
payment_lookup_id: payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: Some(update.payment_preimage),
|
payment_proof: Some(update.payment_preimage),
|
||||||
status: MeltQuoteState::Unknown,
|
status: MeltQuoteState::Unknown,
|
||||||
total_spent: Amount::ZERO,
|
total_spent: Amount::ZERO,
|
||||||
@@ -533,7 +591,7 @@ impl MintPayment for Lnd {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PaymentStatus::Succeeded => MakePaymentResponse {
|
PaymentStatus::Succeeded => MakePaymentResponse {
|
||||||
payment_lookup_id: payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: Some(update.payment_preimage),
|
payment_proof: Some(update.payment_preimage),
|
||||||
status: MeltQuoteState::Paid,
|
status: MeltQuoteState::Paid,
|
||||||
total_spent: Amount::from(
|
total_spent: Amount::from(
|
||||||
@@ -546,7 +604,7 @@ impl MintPayment for Lnd {
|
|||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
},
|
},
|
||||||
PaymentStatus::Failed => MakePaymentResponse {
|
PaymentStatus::Failed => MakePaymentResponse {
|
||||||
payment_lookup_id: payment_hash.to_string(),
|
payment_lookup_id: payment_identifier.clone(),
|
||||||
payment_proof: Some(update.payment_preimage),
|
payment_proof: Some(update.payment_preimage),
|
||||||
status: MeltQuoteState::Failed,
|
status: MeltQuoteState::Failed,
|
||||||
total_spent: Amount::ZERO,
|
total_spent: Amount::ZERO,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ anyhow.workspace = true
|
|||||||
cdk = { workspace = true, features = [
|
cdk = { workspace = true, features = [
|
||||||
"mint",
|
"mint",
|
||||||
] }
|
] }
|
||||||
|
cdk-common = { workspace = true }
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
tonic = { workspace = true, features = ["transport"] }
|
tonic = { workspace = true, features = ["transport"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ use std::path::PathBuf;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use cdk::mint::Mint;
|
use cdk::mint::{Mint, MintQuote};
|
||||||
use cdk::nuts::nut04::MintMethodSettings;
|
use cdk::nuts::nut04::MintMethodSettings;
|
||||||
use cdk::nuts::nut05::MeltMethodSettings;
|
use cdk::nuts::nut05::MeltMethodSettings;
|
||||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
|
use cdk::nuts::{CurrencyUnit, MintQuoteState, PaymentMethod};
|
||||||
use cdk::types::QuoteTTL;
|
use cdk::types::QuoteTTL;
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
|
use cdk_common::payment::WaitPaymentResponse;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
@@ -650,15 +651,47 @@ impl CdkMint for MintRPCServer {
|
|||||||
|
|
||||||
match state {
|
match state {
|
||||||
MintQuoteState::Paid => {
|
MintQuoteState::Paid => {
|
||||||
self.mint
|
// Create a dummy payment response
|
||||||
.pay_mint_quote(&mint_quote)
|
let response = WaitPaymentResponse {
|
||||||
|
payment_id: String::new(),
|
||||||
|
payment_amount: mint_quote.amount_paid(),
|
||||||
|
unit: mint_quote.unit.clone(),
|
||||||
|
payment_identifier: mint_quote.request_lookup_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tx = self
|
||||||
|
.mint
|
||||||
|
.localstore
|
||||||
|
.begin_transaction()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not find quote".to_string()))?;
|
.map_err(|_| Status::internal("Could not start db transaction".to_string()))?;
|
||||||
|
|
||||||
|
self.mint
|
||||||
|
.pay_mint_quote(&mut tx, &mint_quote, response)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::internal("Could not process payment".to_string()))?;
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::internal("Could not commit db transaction".to_string()))?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let mut mint_quote = mint_quote;
|
// Create a new quote with the same values
|
||||||
|
let quote = MintQuote::new(
|
||||||
mint_quote.state = state;
|
Some(mint_quote.id), // id
|
||||||
|
mint_quote.request.clone(), // request
|
||||||
|
mint_quote.unit.clone(), // unit
|
||||||
|
mint_quote.amount, // amount
|
||||||
|
mint_quote.expiry, // expiry
|
||||||
|
mint_quote.request_lookup_id.clone(), // request_lookup_id
|
||||||
|
mint_quote.pubkey, // pubkey
|
||||||
|
mint_quote.amount_issued(), // amount_issued
|
||||||
|
mint_quote.amount_paid(), // amount_paid
|
||||||
|
mint_quote.payment_method.clone(), // method
|
||||||
|
0, // created_at
|
||||||
|
vec![], // blinded_messages
|
||||||
|
vec![], // payment_ids
|
||||||
|
);
|
||||||
|
|
||||||
let mut tx = self
|
let mut tx = self
|
||||||
.mint
|
.mint
|
||||||
@@ -666,7 +699,7 @@ impl CdkMint for MintRPCServer {
|
|||||||
.begin_transaction()
|
.begin_transaction()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not update quote".to_string()))?;
|
.map_err(|_| Status::internal("Could not update quote".to_string()))?;
|
||||||
tx.add_or_replace_mint_quote(mint_quote)
|
tx.add_mint_quote(quote.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not update quote".to_string()))?;
|
.map_err(|_| Status::internal("Could not update quote".to_string()))?;
|
||||||
tx.commit()
|
tx.commit()
|
||||||
@@ -684,7 +717,7 @@ impl CdkMint for MintRPCServer {
|
|||||||
.ok_or(Status::invalid_argument("Could not find quote".to_string()))?;
|
.ok_or(Status::invalid_argument("Could not find quote".to_string()))?;
|
||||||
|
|
||||||
Ok(Response::new(UpdateNut04QuoteRequest {
|
Ok(Response::new(UpdateNut04QuoteRequest {
|
||||||
state: mint_quote.state.to_string(),
|
state: mint_quote.state().to_string(),
|
||||||
quote_id: mint_quote.id.to_string(),
|
quote_id: mint_quote.id.to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,21 @@ async fn configure_backend_for_unit(
|
|||||||
mint_melt_limits: MintMeltLimits,
|
mint_melt_limits: MintMeltLimits,
|
||||||
backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
||||||
) -> Result<MintBuilder> {
|
) -> Result<MintBuilder> {
|
||||||
|
let payment_settings = backend.get_settings().await?;
|
||||||
|
|
||||||
|
if let Some(bolt12) = payment_settings.get("bolt12") {
|
||||||
|
if bolt12.as_bool().unwrap_or_default() {
|
||||||
|
mint_builder = mint_builder
|
||||||
|
.add_ln_backend(
|
||||||
|
unit.clone(),
|
||||||
|
PaymentMethod::Bolt12,
|
||||||
|
mint_melt_limits,
|
||||||
|
Arc::clone(&backend),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mint_builder = mint_builder
|
mint_builder = mint_builder
|
||||||
.add_ln_backend(
|
.add_ln_backend(
|
||||||
unit.clone(),
|
unit.clone(),
|
||||||
@@ -651,39 +666,6 @@ async fn start_services(
|
|||||||
let listen_port = settings.info.listen_port;
|
let listen_port = settings.info.listen_port;
|
||||||
let cache: HttpCache = settings.info.http_cache.clone().into();
|
let cache: HttpCache = settings.info.http_cache.clone().into();
|
||||||
|
|
||||||
let v1_service =
|
|
||||||
cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache).await?;
|
|
||||||
|
|
||||||
let mut mint_service = Router::new()
|
|
||||||
.merge(v1_service)
|
|
||||||
.layer(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(RequestDecompressionLayer::new())
|
|
||||||
.layer(CompressionLayer::new()),
|
|
||||||
)
|
|
||||||
.layer(TraceLayer::new_for_http());
|
|
||||||
|
|
||||||
#[cfg(feature = "swagger")]
|
|
||||||
{
|
|
||||||
if settings.info.enable_swagger_ui.unwrap_or(false) {
|
|
||||||
mint_service = mint_service.merge(
|
|
||||||
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
|
|
||||||
.url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for router in ln_routers {
|
|
||||||
mint_service = mint_service.merge(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
let shutdown = Arc::new(Notify::new());
|
|
||||||
let mint_clone = Arc::clone(&mint);
|
|
||||||
tokio::spawn({
|
|
||||||
let shutdown = Arc::clone(&shutdown);
|
|
||||||
async move { mint_clone.wait_for_paid_invoices(shutdown).await }
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "management-rpc")]
|
#[cfg(feature = "management-rpc")]
|
||||||
let mut rpc_enabled = false;
|
let mut rpc_enabled = false;
|
||||||
#[cfg(not(feature = "management-rpc"))]
|
#[cfg(not(feature = "management-rpc"))]
|
||||||
@@ -742,6 +724,47 @@ async fn start_services(
|
|||||||
mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
|
mint.set_quote_ttl(QuoteTTL::new(10_000, 10_000)).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mint_info = mint.mint_info().await?;
|
||||||
|
let nut04_methods = mint_info.nuts.nut04.supported_methods();
|
||||||
|
let nut05_methods = mint_info.nuts.nut05.supported_methods();
|
||||||
|
|
||||||
|
let bolt12_supported = nut04_methods.contains(&&PaymentMethod::Bolt12)
|
||||||
|
|| nut05_methods.contains(&&PaymentMethod::Bolt12);
|
||||||
|
|
||||||
|
let v1_service =
|
||||||
|
cdk_axum::create_mint_router_with_custom_cache(Arc::clone(&mint), cache, bolt12_supported)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut mint_service = Router::new()
|
||||||
|
.merge(v1_service)
|
||||||
|
.layer(
|
||||||
|
ServiceBuilder::new()
|
||||||
|
.layer(RequestDecompressionLayer::new())
|
||||||
|
.layer(CompressionLayer::new()),
|
||||||
|
)
|
||||||
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
|
#[cfg(feature = "swagger")]
|
||||||
|
{
|
||||||
|
if settings.info.enable_swagger_ui.unwrap_or(false) {
|
||||||
|
mint_service = mint_service.merge(
|
||||||
|
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
|
||||||
|
.url("/api-docs/openapi.json", cdk_axum::ApiDocV1::openapi()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for router in ln_routers {
|
||||||
|
mint_service = mint_service.merge(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown = Arc::new(Notify::new());
|
||||||
|
let mint_clone = Arc::clone(&mint);
|
||||||
|
tokio::spawn({
|
||||||
|
let shutdown = Arc::clone(&shutdown);
|
||||||
|
async move { mint_clone.wait_for_paid_invoices(shutdown).await }
|
||||||
|
});
|
||||||
|
|
||||||
let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
|
let socket_addr = SocketAddr::from_str(&format!("{listen_addr}:{listen_port}"))?;
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(socket_addr).await?;
|
let listener = tokio::net::TcpListener::bind(socket_addr).await?;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ lnd = ["dep:cdk-lnd"]
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
bitcoin.workspace = true
|
bitcoin.workspace = true
|
||||||
|
cashu.workspace = true
|
||||||
cdk-common = { workspace = true, features = ["mint"] }
|
cdk-common = { workspace = true, features = ["mint"] }
|
||||||
cdk-cln = { workspace = true, optional = true }
|
cdk-cln = { workspace = true, optional = true }
|
||||||
cdk-lnd = { workspace = true, optional = true }
|
cdk-lnd = { workspace = true, optional = true }
|
||||||
@@ -34,7 +35,7 @@ thiserror.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
lightning-invoice.workspace = true
|
lightning-invoice.workspace = true
|
||||||
uuid = { workspace = true, optional = true }
|
uuid = { workspace = true }
|
||||||
utoipa = { workspace = true, optional = true }
|
utoipa = { workspace = true, optional = true }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
@@ -43,6 +44,8 @@ tonic = { workspace = true, features = ["router"] }
|
|||||||
prost.workspace = true
|
prost.workspace = true
|
||||||
tokio-stream.workspace = true
|
tokio-stream.workspace = true
|
||||||
tokio-util = { workspace = true, default-features = false }
|
tokio-util = { workspace = true, default-features = false }
|
||||||
|
hex = "0.4"
|
||||||
|
lightning = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Errors
|
//! Error for payment processor
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
/// CDK Payment processor error
|
/// CDK Payment processor error
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -8,13 +9,73 @@ pub enum Error {
|
|||||||
/// Invalid ID
|
/// Invalid ID
|
||||||
#[error("Invalid id")]
|
#[error("Invalid id")]
|
||||||
InvalidId,
|
InvalidId,
|
||||||
|
/// Invalid payment identifier
|
||||||
|
#[error("Invalid payment identifier")]
|
||||||
|
InvalidPaymentIdentifier,
|
||||||
|
/// Invalid hash
|
||||||
|
#[error("Invalid hash")]
|
||||||
|
InvalidHash,
|
||||||
|
/// Invalid currency unit
|
||||||
|
#[error("Invalid currency unit: {0}")]
|
||||||
|
InvalidCurrencyUnit(String),
|
||||||
|
/// Parse invoice error
|
||||||
|
#[error(transparent)]
|
||||||
|
Invoice(#[from] lightning_invoice::ParseOrSemanticError),
|
||||||
|
/// Hex decode error
|
||||||
|
#[error(transparent)]
|
||||||
|
Hex(#[from] hex::FromHexError),
|
||||||
|
/// BOLT12 parse error
|
||||||
|
#[error("BOLT12 parse error")]
|
||||||
|
Bolt12Parse,
|
||||||
/// NUT00 Error
|
/// NUT00 Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT00(#[from] cdk_common::nuts::nut00::Error),
|
NUT00(#[from] cdk_common::nuts::nut00::Error),
|
||||||
/// NUT05 error
|
/// NUT05 error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT05(#[from] cdk_common::nuts::nut05::Error),
|
NUT05(#[from] cdk_common::nuts::nut05::Error),
|
||||||
/// Parse invoice error
|
/// Payment error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Invoice(#[from] lightning_invoice::ParseOrSemanticError),
|
Payment(#[from] cdk_common::payment::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for Status {
|
||||||
|
fn from(error: Error) -> Self {
|
||||||
|
match error {
|
||||||
|
Error::InvalidId => Status::invalid_argument("Invalid ID"),
|
||||||
|
Error::InvalidPaymentIdentifier => {
|
||||||
|
Status::invalid_argument("Invalid payment identifier")
|
||||||
|
}
|
||||||
|
Error::InvalidHash => Status::invalid_argument("Invalid hash"),
|
||||||
|
Error::InvalidCurrencyUnit(unit) => {
|
||||||
|
Status::invalid_argument(format!("Invalid currency unit: {unit}"))
|
||||||
|
}
|
||||||
|
Error::Invoice(err) => Status::invalid_argument(format!("Invoice error: {err}")),
|
||||||
|
Error::Hex(err) => Status::invalid_argument(format!("Hex decode error: {err}")),
|
||||||
|
Error::Bolt12Parse => Status::invalid_argument("BOLT12 parse error"),
|
||||||
|
Error::NUT00(err) => Status::internal(format!("NUT00 error: {err}")),
|
||||||
|
Error::NUT05(err) => Status::internal(format!("NUT05 error: {err}")),
|
||||||
|
Error::Payment(err) => Status::internal(format!("Payment error: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for cdk_common::payment::Error {
|
||||||
|
fn from(error: Error) -> Self {
|
||||||
|
match error {
|
||||||
|
Error::InvalidId => Self::Custom("Invalid ID".to_string()),
|
||||||
|
Error::InvalidPaymentIdentifier => {
|
||||||
|
Self::Custom("Invalid payment identifier".to_string())
|
||||||
|
}
|
||||||
|
Error::InvalidHash => Self::Custom("Invalid hash".to_string()),
|
||||||
|
Error::InvalidCurrencyUnit(unit) => {
|
||||||
|
Self::Custom(format!("Invalid currency unit: {unit}"))
|
||||||
|
}
|
||||||
|
Error::Invoice(err) => Self::Custom(format!("Invoice error: {err}")),
|
||||||
|
Error::Hex(err) => Self::Custom(format!("Hex decode error: {err}")),
|
||||||
|
Error::Bolt12Parse => Self::Custom("BOLT12 parse error".to_string()),
|
||||||
|
Error::NUT00(err) => Self::Custom(format!("NUT00 error: {err}")),
|
||||||
|
Error::NUT05(err) => err.into(),
|
||||||
|
Error::Payment(err) => err,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#![warn(rustdoc::bare_urls)]
|
#![warn(rustdoc::bare_urls)]
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
/// Protocol types and functionality for the CDK payment processor
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
|
||||||
pub use proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
|
pub use proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
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;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use cdk_common::payment::{
|
use cdk_common::payment::{
|
||||||
CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse, MintPayment,
|
CreateIncomingPaymentResponse, IncomingPaymentOptions as CdkIncomingPaymentOptions,
|
||||||
PaymentQuoteResponse,
|
MakePaymentResponse as CdkMakePaymentResponse, MintPayment,
|
||||||
|
PaymentQuoteResponse as CdkPaymentQuoteResponse, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk_common::{mint, Amount, CurrencyUnit, MeltOptions, MintQuoteState};
|
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -17,10 +16,10 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
|
|||||||
use tonic::{async_trait, Request};
|
use tonic::{async_trait, Request};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::cdk_payment_processor_client::CdkPaymentProcessorClient;
|
use crate::proto::cdk_payment_processor_client::CdkPaymentProcessorClient;
|
||||||
use super::{
|
use crate::proto::{
|
||||||
CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest,
|
CheckIncomingPaymentRequest, CheckOutgoingPaymentRequest, CreatePaymentRequest, EmptyRequest,
|
||||||
MakePaymentRequest, SettingsRequest, WaitIncomingPaymentRequest,
|
IncomingPaymentOptions, MakePaymentRequest, OutgoingPaymentRequestType, PaymentQuoteRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Payment Processor
|
/// Payment Processor
|
||||||
@@ -100,7 +99,7 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
async fn get_settings(&self) -> Result<Value, Self::Err> {
|
async fn get_settings(&self) -> Result<Value, Self::Err> {
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
let response = inner
|
let response = inner
|
||||||
.get_settings(Request::new(SettingsRequest {}))
|
.get_settings(Request::new(EmptyRequest {}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not get settings: {}", err);
|
tracing::error!("Could not get settings: {}", err);
|
||||||
@@ -115,18 +114,36 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
/// Create a new invoice
|
/// Create a new invoice
|
||||||
async fn create_incoming_payment_request(
|
async fn create_incoming_payment_request(
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
unit: &cdk_common::CurrencyUnit,
|
||||||
unit: &CurrencyUnit,
|
options: CdkIncomingPaymentOptions,
|
||||||
description: String,
|
|
||||||
unix_expiry: Option<u64>,
|
|
||||||
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
|
let proto_options = match options {
|
||||||
|
CdkIncomingPaymentOptions::Bolt11(opts) => IncomingPaymentOptions {
|
||||||
|
options: Some(super::incoming_payment_options::Options::Bolt11(
|
||||||
|
super::Bolt11IncomingPaymentOptions {
|
||||||
|
description: opts.description,
|
||||||
|
amount: opts.amount.into(),
|
||||||
|
unix_expiry: opts.unix_expiry,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
CdkIncomingPaymentOptions::Bolt12(opts) => IncomingPaymentOptions {
|
||||||
|
options: Some(super::incoming_payment_options::Options::Bolt12(
|
||||||
|
super::Bolt12IncomingPaymentOptions {
|
||||||
|
description: opts.description,
|
||||||
|
amount: opts.amount.map(Into::into),
|
||||||
|
unix_expiry: opts.unix_expiry,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let response = inner
|
let response = inner
|
||||||
.create_payment(Request::new(CreatePaymentRequest {
|
.create_payment(Request::new(CreatePaymentRequest {
|
||||||
amount: amount.into(),
|
|
||||||
unit: unit.to_string(),
|
unit: unit.to_string(),
|
||||||
description,
|
options: Some(proto_options),
|
||||||
unix_expiry,
|
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -143,16 +160,36 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
|
|
||||||
async fn get_payment_quote(
|
async fn get_payment_quote(
|
||||||
&self,
|
&self,
|
||||||
request: &str,
|
unit: &cdk_common::CurrencyUnit,
|
||||||
unit: &CurrencyUnit,
|
options: cdk_common::payment::OutgoingPaymentOptions,
|
||||||
options: Option<MeltOptions>,
|
) -> Result<CdkPaymentQuoteResponse, Self::Err> {
|
||||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
|
let request_type = match &options {
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt11(_) => {
|
||||||
|
OutgoingPaymentRequestType::Bolt11Invoice
|
||||||
|
}
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt12(_) => {
|
||||||
|
OutgoingPaymentRequestType::Bolt12Offer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proto_request = match &options {
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.bolt11.to_string(),
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.offer.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let proto_options = match &options {
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => opts.melt_options,
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => opts.melt_options,
|
||||||
|
};
|
||||||
|
|
||||||
let response = inner
|
let response = inner
|
||||||
.get_payment_quote(Request::new(super::PaymentQuoteRequest {
|
.get_payment_quote(Request::new(PaymentQuoteRequest {
|
||||||
request: request.to_string(),
|
request: proto_request,
|
||||||
unit: unit.to_string(),
|
unit: unit.to_string(),
|
||||||
options: options.map(|o| o.into()),
|
options: proto_options.map(Into::into),
|
||||||
|
request_type: request_type.into(),
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -167,16 +204,44 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
|
|
||||||
async fn make_payment(
|
async fn make_payment(
|
||||||
&self,
|
&self,
|
||||||
melt_quote: mint::MeltQuote,
|
_unit: &cdk_common::CurrencyUnit,
|
||||||
partial_amount: Option<Amount>,
|
options: cdk_common::payment::OutgoingPaymentOptions,
|
||||||
max_fee_amount: Option<Amount>,
|
|
||||||
) -> Result<CdkMakePaymentResponse, Self::Err> {
|
) -> Result<CdkMakePaymentResponse, Self::Err> {
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
|
|
||||||
|
let payment_options = match options {
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt11(opts) => {
|
||||||
|
super::OutgoingPaymentVariant {
|
||||||
|
options: Some(super::outgoing_payment_variant::Options::Bolt11(
|
||||||
|
super::Bolt11OutgoingPaymentOptions {
|
||||||
|
bolt11: opts.bolt11.to_string(),
|
||||||
|
max_fee_amount: opts.max_fee_amount.map(Into::into),
|
||||||
|
timeout_secs: opts.timeout_secs,
|
||||||
|
melt_options: opts.melt_options.map(Into::into),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt12(opts) => {
|
||||||
|
super::OutgoingPaymentVariant {
|
||||||
|
options: Some(super::outgoing_payment_variant::Options::Bolt12(
|
||||||
|
super::Bolt12OutgoingPaymentOptions {
|
||||||
|
offer: opts.offer.to_string(),
|
||||||
|
max_fee_amount: opts.max_fee_amount.map(Into::into),
|
||||||
|
timeout_secs: opts.timeout_secs,
|
||||||
|
invoice: opts.invoice,
|
||||||
|
melt_options: opts.melt_options.map(Into::into),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let response = inner
|
let response = inner
|
||||||
.make_payment(Request::new(MakePaymentRequest {
|
.make_payment(Request::new(MakePaymentRequest {
|
||||||
melt_quote: Some(melt_quote.into()),
|
payment_options: Some(payment_options),
|
||||||
partial_amount: partial_amount.map(|a| a.into()),
|
partial_amount: None,
|
||||||
max_fee_amount: max_fee_amount.map(|a| a.into()),
|
max_fee_amount: None,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -198,17 +263,16 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Listen for invoices to be paid to the mint
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn wait_any_incoming_payment(
|
async fn wait_any_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
|
) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
|
||||||
self.wait_incoming_payment_stream_is_active
|
self.wait_incoming_payment_stream_is_active
|
||||||
.store(true, Ordering::SeqCst);
|
.store(true, Ordering::SeqCst);
|
||||||
tracing::debug!("Client waiting for payment");
|
tracing::debug!("Client waiting for payment");
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
let stream = inner
|
let stream = inner
|
||||||
.wait_incoming_payment(WaitIncomingPaymentRequest {})
|
.wait_incoming_payment(EmptyRequest {})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not check incoming payment stream: {}", err);
|
tracing::error!("Could not check incoming payment stream: {}", err);
|
||||||
@@ -222,15 +286,18 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
|
|
||||||
let transformed_stream = stream
|
let transformed_stream = stream
|
||||||
.take_until(cancel_fut)
|
.take_until(cancel_fut)
|
||||||
.filter_map(|item| async move {
|
.filter_map(|item| async {
|
||||||
match item {
|
match item {
|
||||||
Ok(value) => {
|
Ok(value) => match value.try_into() {
|
||||||
tracing::warn!("{}", value.lookup_id);
|
Ok(payment_response) => Some(payment_response),
|
||||||
Some(value.lookup_id)
|
Err(e) => {
|
||||||
|
tracing::error!("Error converting payment response: {}", e);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Error in payment stream: {}", e);
|
tracing::error!("Error in payment stream: {}", e);
|
||||||
None // Skip this item and continue with the stream
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -255,12 +322,12 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
|
|
||||||
async fn check_incoming_payment_status(
|
async fn check_incoming_payment_status(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
payment_identifier: &cdk_common::payment::PaymentIdentifier,
|
||||||
) -> Result<MintQuoteState, Self::Err> {
|
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
let response = inner
|
let response = inner
|
||||||
.check_incoming_payment(Request::new(CheckIncomingPaymentRequest {
|
.check_incoming_payment(Request::new(CheckIncomingPaymentRequest {
|
||||||
request_lookup_id: request_lookup_id.to_string(),
|
request_identifier: Some(payment_identifier.clone().into()),
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -269,20 +336,21 @@ impl MintPayment for PaymentProcessorClient {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let check_incoming = response.into_inner();
|
let check_incoming = response.into_inner();
|
||||||
|
check_incoming
|
||||||
let status = check_incoming.status().as_str_name();
|
.payments
|
||||||
|
.into_iter()
|
||||||
Ok(MintQuoteState::from_str(status)?)
|
.map(|resp| resp.try_into().map_err(Self::Err::from))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_outgoing_payment(
|
async fn check_outgoing_payment(
|
||||||
&self,
|
&self,
|
||||||
request_lookup_id: &str,
|
payment_identifier: &cdk_common::payment::PaymentIdentifier,
|
||||||
) -> Result<CdkMakePaymentResponse, Self::Err> {
|
) -> Result<CdkMakePaymentResponse, Self::Err> {
|
||||||
let mut inner = self.inner.clone();
|
let mut inner = self.inner.clone();
|
||||||
let response = inner
|
let response = inner
|
||||||
.check_outgoing_payment(Request::new(CheckOutgoingPaymentRequest {
|
.check_outgoing_payment(Request::new(CheckOutgoingPaymentRequest {
|
||||||
request_lookup_id: request_lookup_id.to_string(),
|
request_identifier: Some(payment_identifier.clone().into()),
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
//! Proto types for payment processor
|
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use cdk_common::payment::{
|
use cdk_common::payment::{
|
||||||
CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
|
CreateIncomingPaymentResponse, MakePaymentResponse as CdkMakePaymentResponse,
|
||||||
|
PaymentIdentifier as CdkPaymentIdentifier, WaitPaymentResponse,
|
||||||
};
|
};
|
||||||
use cdk_common::{Bolt11Invoice, CurrencyUnit, MeltQuoteBolt11Request};
|
use cdk_common::{CurrencyUnit, MeltOptions as CdkMeltOptions};
|
||||||
use melt_options::Options;
|
|
||||||
mod client;
|
mod client;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
@@ -15,15 +14,85 @@ pub use server::PaymentProcessorServer;
|
|||||||
|
|
||||||
tonic::include_proto!("cdk_payment_processor");
|
tonic::include_proto!("cdk_payment_processor");
|
||||||
|
|
||||||
|
impl From<CdkPaymentIdentifier> for PaymentIdentifier {
|
||||||
|
fn from(value: CdkPaymentIdentifier) -> Self {
|
||||||
|
match value {
|
||||||
|
CdkPaymentIdentifier::Label(id) => Self {
|
||||||
|
r#type: PaymentIdentifierType::Label.into(),
|
||||||
|
value: Some(payment_identifier::Value::Id(id)),
|
||||||
|
},
|
||||||
|
CdkPaymentIdentifier::OfferId(id) => Self {
|
||||||
|
r#type: PaymentIdentifierType::OfferId.into(),
|
||||||
|
value: Some(payment_identifier::Value::Id(id)),
|
||||||
|
},
|
||||||
|
CdkPaymentIdentifier::PaymentHash(hash) => Self {
|
||||||
|
r#type: PaymentIdentifierType::PaymentHash.into(),
|
||||||
|
value: Some(payment_identifier::Value::Hash(hex::encode(hash))),
|
||||||
|
},
|
||||||
|
CdkPaymentIdentifier::Bolt12PaymentHash(hash) => Self {
|
||||||
|
r#type: PaymentIdentifierType::Bolt12PaymentHash.into(),
|
||||||
|
value: Some(payment_identifier::Value::Hash(hex::encode(hash))),
|
||||||
|
},
|
||||||
|
CdkPaymentIdentifier::CustomId(id) => Self {
|
||||||
|
r#type: PaymentIdentifierType::CustomId.into(),
|
||||||
|
value: Some(payment_identifier::Value::Id(id)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<PaymentIdentifier> for CdkPaymentIdentifier {
|
||||||
|
type Error = crate::error::Error;
|
||||||
|
|
||||||
|
fn try_from(value: PaymentIdentifier) -> Result<Self, Self::Error> {
|
||||||
|
match (value.r#type(), value.value) {
|
||||||
|
(PaymentIdentifierType::Label, Some(payment_identifier::Value::Id(id))) => {
|
||||||
|
Ok(CdkPaymentIdentifier::Label(id))
|
||||||
|
}
|
||||||
|
(PaymentIdentifierType::OfferId, Some(payment_identifier::Value::Id(id))) => {
|
||||||
|
Ok(CdkPaymentIdentifier::OfferId(id))
|
||||||
|
}
|
||||||
|
(PaymentIdentifierType::PaymentHash, Some(payment_identifier::Value::Hash(hash))) => {
|
||||||
|
let decoded = hex::decode(hash)?;
|
||||||
|
let hash_array: [u8; 32] = decoded
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| crate::error::Error::InvalidHash)?;
|
||||||
|
Ok(CdkPaymentIdentifier::PaymentHash(hash_array))
|
||||||
|
}
|
||||||
|
(
|
||||||
|
PaymentIdentifierType::Bolt12PaymentHash,
|
||||||
|
Some(payment_identifier::Value::Hash(hash)),
|
||||||
|
) => {
|
||||||
|
let decoded = hex::decode(hash)?;
|
||||||
|
let hash_array: [u8; 32] = decoded
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| crate::error::Error::InvalidHash)?;
|
||||||
|
Ok(CdkPaymentIdentifier::Bolt12PaymentHash(hash_array))
|
||||||
|
}
|
||||||
|
(PaymentIdentifierType::CustomId, Some(payment_identifier::Value::Id(id))) => {
|
||||||
|
Ok(CdkPaymentIdentifier::CustomId(id))
|
||||||
|
}
|
||||||
|
_ => Err(crate::error::Error::InvalidPaymentIdentifier),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
|
impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
|
||||||
type Error = crate::error::Error;
|
type Error = crate::error::Error;
|
||||||
fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: MakePaymentResponse) -> Result<Self, Self::Error> {
|
||||||
|
let status = value.status().as_str_name().parse()?;
|
||||||
|
let payment_proof = value.payment_proof;
|
||||||
|
let total_spent = value.total_spent.into();
|
||||||
|
let unit = CurrencyUnit::from_str(&value.unit)?;
|
||||||
|
let payment_identifier = value
|
||||||
|
.payment_identifier
|
||||||
|
.ok_or(crate::error::Error::InvalidPaymentIdentifier)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
payment_lookup_id: value.payment_lookup_id.clone(),
|
payment_lookup_id: payment_identifier.try_into()?,
|
||||||
payment_proof: value.payment_proof.clone(),
|
payment_proof,
|
||||||
status: value.status().as_str_name().parse()?,
|
status,
|
||||||
total_spent: value.total_spent.into(),
|
total_spent,
|
||||||
unit: value.unit.parse()?,
|
unit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,8 +100,8 @@ impl TryFrom<MakePaymentResponse> for CdkMakePaymentResponse {
|
|||||||
impl From<CdkMakePaymentResponse> for MakePaymentResponse {
|
impl From<CdkMakePaymentResponse> for MakePaymentResponse {
|
||||||
fn from(value: CdkMakePaymentResponse) -> Self {
|
fn from(value: CdkMakePaymentResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
payment_lookup_id: value.payment_lookup_id.clone(),
|
payment_identifier: Some(value.payment_lookup_id.into()),
|
||||||
payment_proof: value.payment_proof.clone(),
|
payment_proof: value.payment_proof,
|
||||||
status: QuoteState::from(value.status).into(),
|
status: QuoteState::from(value.status).into(),
|
||||||
total_spent: value.total_spent.into(),
|
total_spent: value.total_spent.into(),
|
||||||
unit: value.unit.to_string(),
|
unit: value.unit.to_string(),
|
||||||
@@ -43,8 +112,8 @@ impl From<CdkMakePaymentResponse> for MakePaymentResponse {
|
|||||||
impl From<CreateIncomingPaymentResponse> for CreatePaymentResponse {
|
impl From<CreateIncomingPaymentResponse> for CreatePaymentResponse {
|
||||||
fn from(value: CreateIncomingPaymentResponse) -> Self {
|
fn from(value: CreateIncomingPaymentResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
request_lookup_id: value.request_lookup_id,
|
request_identifier: Some(value.request_lookup_id.into()),
|
||||||
request: value.request.to_string(),
|
request: value.request,
|
||||||
expiry: value.expiry,
|
expiry: value.expiry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,82 +123,102 @@ impl TryFrom<CreatePaymentResponse> for CreateIncomingPaymentResponse {
|
|||||||
type Error = crate::error::Error;
|
type Error = crate::error::Error;
|
||||||
|
|
||||||
fn try_from(value: CreatePaymentResponse) -> Result<Self, Self::Error> {
|
fn try_from(value: CreatePaymentResponse) -> Result<Self, Self::Error> {
|
||||||
|
let request_identifier = value
|
||||||
|
.request_identifier
|
||||||
|
.ok_or(crate::error::Error::InvalidPaymentIdentifier)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
request_lookup_id: value.request_lookup_id,
|
request_lookup_id: request_identifier.try_into()?,
|
||||||
request: value.request,
|
request: value.request,
|
||||||
expiry: value.expiry,
|
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 {
|
impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
|
||||||
fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self {
|
fn from(value: cdk_common::payment::PaymentQuoteResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
request_lookup_id: value.request_lookup_id,
|
request_identifier: Some(value.request_lookup_id.into()),
|
||||||
amount: value.amount.into(),
|
amount: value.amount.into(),
|
||||||
fee: value.fee.into(),
|
fee: value.fee.into(),
|
||||||
state: QuoteState::from(value.state).into(),
|
|
||||||
unit: value.unit.to_string(),
|
unit: value.unit.to_string(),
|
||||||
|
state: QuoteState::from(value.state).into(),
|
||||||
|
melt_options: value.options.map(|opt| match opt {
|
||||||
|
cdk_common::payment::PaymentQuoteOptions::Bolt12 { invoice } => {
|
||||||
|
PaymentQuoteOptions {
|
||||||
|
melt_options: Some(payment_quote_options::MeltOptions::Bolt12(
|
||||||
|
Bolt12Options {
|
||||||
|
invoice: invoice.map(String::from_utf8).and_then(|r| r.ok()),
|
||||||
|
},
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<cdk_common::nut23::MeltOptions> for MeltOptions {
|
|
||||||
fn from(value: cdk_common::nut23::MeltOptions) -> Self {
|
|
||||||
Self {
|
|
||||||
options: Some(value.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<cdk_common::nut23::MeltOptions> for Options {
|
|
||||||
fn from(value: cdk_common::nut23::MeltOptions) -> Self {
|
|
||||||
match value {
|
|
||||||
cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
|
|
||||||
amount: mpp.amount.into(),
|
|
||||||
}),
|
}),
|
||||||
cdk_common::MeltOptions::Amountless { amountless } => Self::Amountless(Amountless {
|
|
||||||
amount_msat: amountless.amount_msat.into(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MeltOptions> for cdk_common::nut23::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),
|
|
||||||
Options::Amountless(amountless) => {
|
|
||||||
cdk_common::MeltOptions::new_amountless(amountless.amount_msat)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
|
impl From<PaymentQuoteResponse> for cdk_common::payment::PaymentQuoteResponse {
|
||||||
fn from(value: PaymentQuoteResponse) -> Self {
|
fn from(value: PaymentQuoteResponse) -> Self {
|
||||||
|
let state_val = value.state();
|
||||||
|
let request_identifier = value
|
||||||
|
.request_identifier
|
||||||
|
.expect("request identifier required");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
request_lookup_id: value.request_lookup_id.clone(),
|
request_lookup_id: request_identifier
|
||||||
|
.try_into()
|
||||||
|
.expect("valid request identifier"),
|
||||||
amount: value.amount.into(),
|
amount: value.amount.into(),
|
||||||
unit: CurrencyUnit::from_str(&value.unit).unwrap_or_default(),
|
|
||||||
fee: value.fee.into(),
|
fee: value.fee.into(),
|
||||||
state: value.state().into(),
|
unit: CurrencyUnit::from_str(&value.unit).unwrap_or_default(),
|
||||||
|
state: state_val.into(),
|
||||||
|
options: value.melt_options.map(|opt| match opt.melt_options {
|
||||||
|
Some(payment_quote_options::MeltOptions::Bolt12(bolt12)) => {
|
||||||
|
cdk_common::payment::PaymentQuoteOptions::Bolt12 {
|
||||||
|
invoice: bolt12.invoice.as_deref().map(str::as_bytes).map(Vec::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => unreachable!(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<QuoteState> for cdk_common::nut05::QuoteState {
|
impl From<MeltOptions> for CdkMeltOptions {
|
||||||
|
fn from(value: MeltOptions) -> Self {
|
||||||
|
match value.options.expect("option defined") {
|
||||||
|
melt_options::Options::Mpp(mpp) => Self::Mpp {
|
||||||
|
mpp: cashu::nuts::nut15::Mpp {
|
||||||
|
amount: mpp.amount.into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
melt_options::Options::Amountless(amountless) => Self::Amountless {
|
||||||
|
amountless: cashu::nuts::nut23::Amountless {
|
||||||
|
amount_msat: amountless.amount_msat.into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CdkMeltOptions> for MeltOptions {
|
||||||
|
fn from(value: CdkMeltOptions) -> Self {
|
||||||
|
match value {
|
||||||
|
CdkMeltOptions::Mpp { mpp } => Self {
|
||||||
|
options: Some(melt_options::Options::Mpp(Mpp {
|
||||||
|
amount: mpp.amount.into(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
CdkMeltOptions::Amountless { amountless } => Self {
|
||||||
|
options: Some(melt_options::Options::Amountless(Amountless {
|
||||||
|
amount_msat: amountless.amount_msat.into(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QuoteState> for cdk_common::nuts::MeltQuoteState {
|
||||||
fn from(value: QuoteState) -> Self {
|
fn from(value: QuoteState) -> Self {
|
||||||
match value {
|
match value {
|
||||||
QuoteState::Unpaid => Self::Unpaid,
|
QuoteState::Unpaid => Self::Unpaid,
|
||||||
@@ -142,80 +231,53 @@ impl From<QuoteState> for cdk_common::nut05::QuoteState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<cdk_common::nut05::QuoteState> for QuoteState {
|
impl From<cdk_common::nuts::MeltQuoteState> for QuoteState {
|
||||||
fn from(value: cdk_common::nut05::QuoteState) -> Self {
|
fn from(value: cdk_common::nuts::MeltQuoteState) -> Self {
|
||||||
match value {
|
match value {
|
||||||
cdk_common::MeltQuoteState::Unpaid => Self::Unpaid,
|
cdk_common::nuts::MeltQuoteState::Unpaid => Self::Unpaid,
|
||||||
cdk_common::MeltQuoteState::Paid => Self::Paid,
|
cdk_common::nuts::MeltQuoteState::Paid => Self::Paid,
|
||||||
cdk_common::MeltQuoteState::Pending => Self::Pending,
|
cdk_common::nuts::MeltQuoteState::Pending => Self::Pending,
|
||||||
cdk_common::MeltQuoteState::Unknown => Self::Unknown,
|
cdk_common::nuts::MeltQuoteState::Unknown => Self::Unknown,
|
||||||
cdk_common::MeltQuoteState::Failed => Self::Failed,
|
cdk_common::nuts::MeltQuoteState::Failed => Self::Failed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<cdk_common::nut23::QuoteState> for QuoteState {
|
impl From<cdk_common::nuts::MintQuoteState> for QuoteState {
|
||||||
fn from(value: cdk_common::nut23::QuoteState) -> Self {
|
fn from(value: cdk_common::nuts::MintQuoteState) -> Self {
|
||||||
match value {
|
match value {
|
||||||
cdk_common::MintQuoteState::Unpaid => Self::Unpaid,
|
cdk_common::nuts::MintQuoteState::Unpaid => Self::Unpaid,
|
||||||
cdk_common::MintQuoteState::Paid => Self::Paid,
|
cdk_common::nuts::MintQuoteState::Paid => Self::Paid,
|
||||||
cdk_common::MintQuoteState::Pending => Self::Pending,
|
cdk_common::nuts::MintQuoteState::Issued => Self::Issued,
|
||||||
cdk_common::MintQuoteState::Issued => Self::Issued,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<cdk_common::mint::MeltQuote> for MeltQuote {
|
impl From<WaitPaymentResponse> for WaitIncomingPaymentResponse {
|
||||||
fn from(value: cdk_common::mint::MeltQuote) -> Self {
|
fn from(value: WaitPaymentResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id.to_string(),
|
payment_identifier: Some(value.payment_identifier.into()),
|
||||||
|
payment_amount: value.payment_amount.into(),
|
||||||
unit: value.unit.to_string(),
|
unit: value.unit.to_string(),
|
||||||
amount: value.amount.into(),
|
payment_id: value.payment_id,
|
||||||
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()),
|
|
||||||
created_time: value.created_time,
|
|
||||||
paid_time: value.paid_time,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<MeltQuote> for cdk_common::mint::MeltQuote {
|
impl TryFrom<WaitIncomingPaymentResponse> for WaitPaymentResponse {
|
||||||
type Error = crate::error::Error;
|
type Error = crate::error::Error;
|
||||||
|
|
||||||
fn try_from(value: MeltQuote) -> Result<Self, Self::Error> {
|
fn try_from(value: WaitIncomingPaymentResponse) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
let payment_identifier = value
|
||||||
id: value
|
.payment_identifier
|
||||||
.id
|
.ok_or(crate::error::Error::InvalidPaymentIdentifier)?
|
||||||
.parse()
|
.try_into()?;
|
||||||
.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()),
|
|
||||||
created_time: value.created_time,
|
|
||||||
paid_time: value.paid_time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<PaymentQuoteRequest> for MeltQuoteBolt11Request {
|
|
||||||
type Error = crate::error::Error;
|
|
||||||
|
|
||||||
fn try_from(value: PaymentQuoteRequest) -> Result<Self, Self::Error> {
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
request: Bolt11Invoice::from_str(&value.request)?,
|
payment_identifier,
|
||||||
|
payment_amount: value.payment_amount.into(),
|
||||||
unit: CurrencyUnit::from_str(&value.unit)?,
|
unit: CurrencyUnit::from_str(&value.unit)?,
|
||||||
options: value.options.map(|o| o.into()),
|
payment_id: value.payment_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,73 @@ syntax = "proto3";
|
|||||||
package cdk_payment_processor;
|
package cdk_payment_processor;
|
||||||
|
|
||||||
service CdkPaymentProcessor {
|
service CdkPaymentProcessor {
|
||||||
rpc GetSettings(SettingsRequest) returns (SettingsResponse) {}
|
rpc GetSettings(EmptyRequest) returns (SettingsResponse) {}
|
||||||
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {}
|
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {}
|
||||||
rpc GetPaymentQuote(PaymentQuoteRequest) returns (PaymentQuoteResponse) {}
|
rpc GetPaymentQuote(PaymentQuoteRequest) returns (PaymentQuoteResponse) {}
|
||||||
rpc MakePayment(MakePaymentRequest) returns (MakePaymentResponse) {}
|
rpc MakePayment(MakePaymentRequest) returns (MakePaymentResponse) {}
|
||||||
rpc CheckIncomingPayment(CheckIncomingPaymentRequest) returns (CheckIncomingPaymentResponse) {}
|
rpc CheckIncomingPayment(CheckIncomingPaymentRequest) returns (CheckIncomingPaymentResponse) {}
|
||||||
rpc CheckOutgoingPayment(CheckOutgoingPaymentRequest) returns (MakePaymentResponse) {}
|
rpc CheckOutgoingPayment(CheckOutgoingPaymentRequest) returns (MakePaymentResponse) {}
|
||||||
rpc WaitIncomingPayment(WaitIncomingPaymentRequest) returns (stream WaitIncomingPaymentResponse) {}
|
rpc WaitIncomingPayment(EmptyRequest) returns (stream WaitIncomingPaymentResponse) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
message SettingsRequest {}
|
message EmptyRequest {}
|
||||||
|
|
||||||
message SettingsResponse {
|
message SettingsResponse {
|
||||||
string inner = 1;
|
string inner = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Bolt11IncomingPaymentOptions {
|
||||||
|
optional string description = 1;
|
||||||
|
uint64 amount = 2;
|
||||||
|
optional uint64 unix_expiry = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Bolt12IncomingPaymentOptions {
|
||||||
|
optional string description = 1;
|
||||||
|
optional uint64 amount = 2;
|
||||||
|
optional uint64 unix_expiry = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentMethodType {
|
||||||
|
BOLT11 = 0;
|
||||||
|
BOLT12 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OutgoingPaymentRequestType {
|
||||||
|
BOLT11_INVOICE = 0;
|
||||||
|
BOLT12_OFFER = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentIdentifierType {
|
||||||
|
PAYMENT_HASH = 0;
|
||||||
|
OFFER_ID = 1;
|
||||||
|
LABEL = 2;
|
||||||
|
BOLT12_PAYMENT_HASH = 3;
|
||||||
|
CUSTOM_ID = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaymentIdentifier {
|
||||||
|
PaymentIdentifierType type = 1;
|
||||||
|
oneof value {
|
||||||
|
string hash = 2; // Used for PAYMENT_HASH and BOLT12_PAYMENT_HASH
|
||||||
|
string id = 3; // Used for OFFER_ID, LABEL, and CUSTOM_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message IncomingPaymentOptions {
|
||||||
|
oneof options {
|
||||||
|
Bolt11IncomingPaymentOptions bolt11 = 1;
|
||||||
|
Bolt12IncomingPaymentOptions bolt12 = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message CreatePaymentRequest {
|
message CreatePaymentRequest {
|
||||||
uint64 amount = 1;
|
string unit = 1;
|
||||||
string unit = 2;
|
IncomingPaymentOptions options = 2;
|
||||||
string description = 3;
|
|
||||||
optional uint64 unix_expiry = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreatePaymentResponse {
|
message CreatePaymentResponse {
|
||||||
string request_lookup_id = 1;
|
PaymentIdentifier request_identifier = 1;
|
||||||
string request = 2;
|
string request = 2;
|
||||||
optional uint64 expiry = 3;
|
optional uint64 expiry = 3;
|
||||||
}
|
}
|
||||||
@@ -35,7 +78,6 @@ message Mpp {
|
|||||||
uint64 amount = 1;
|
uint64 amount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message Amountless {
|
message Amountless {
|
||||||
uint64 amount_msat = 1;
|
uint64 amount_msat = 1;
|
||||||
}
|
}
|
||||||
@@ -51,6 +93,7 @@ message PaymentQuoteRequest {
|
|||||||
string request = 1;
|
string request = 1;
|
||||||
string unit = 2;
|
string unit = 2;
|
||||||
optional MeltOptions options = 3;
|
optional MeltOptions options = 3;
|
||||||
|
OutgoingPaymentRequestType request_type = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum QuoteState {
|
enum QuoteState {
|
||||||
@@ -62,38 +105,60 @@ enum QuoteState {
|
|||||||
ISSUED = 5;
|
ISSUED = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Bolt12Options {
|
||||||
|
optional string invoice = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaymentQuoteOptions {
|
||||||
|
oneof melt_options {
|
||||||
|
Bolt12Options bolt12 = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message PaymentQuoteResponse {
|
message PaymentQuoteResponse {
|
||||||
string request_lookup_id = 1;
|
PaymentIdentifier request_identifier = 1;
|
||||||
uint64 amount = 2;
|
uint64 amount = 2;
|
||||||
uint64 fee = 3;
|
uint64 fee = 3;
|
||||||
QuoteState state = 4;
|
QuoteState state = 4;
|
||||||
string unit = 5;
|
optional PaymentQuoteOptions melt_options = 5;
|
||||||
|
string unit = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MeltQuote {
|
message Bolt11OutgoingPaymentOptions {
|
||||||
string id = 1;
|
string bolt11 = 1;
|
||||||
string unit = 2;
|
optional uint64 max_fee_amount = 2;
|
||||||
uint64 amount = 3;
|
optional uint64 timeout_secs = 3;
|
||||||
string request = 4;
|
optional MeltOptions melt_options = 4;
|
||||||
uint64 fee_reserve = 5;
|
}
|
||||||
QuoteState state = 6;
|
|
||||||
uint64 expiry = 7;
|
message Bolt12OutgoingPaymentOptions {
|
||||||
optional string payment_preimage = 8;
|
string offer = 1;
|
||||||
string request_lookup_id = 9;
|
optional uint64 max_fee_amount = 2;
|
||||||
optional uint64 msat_to_pay = 10;
|
optional uint64 timeout_secs = 3;
|
||||||
uint64 created_time = 11;
|
optional bytes invoice = 4;
|
||||||
optional uint64 paid_time = 12;
|
optional MeltOptions melt_options = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OutgoingPaymentOptionsType {
|
||||||
|
OUTGOING_BOLT11 = 0;
|
||||||
|
OUTGOING_BOLT12 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OutgoingPaymentVariant {
|
||||||
|
oneof options {
|
||||||
|
Bolt11OutgoingPaymentOptions bolt11 = 1;
|
||||||
|
Bolt12OutgoingPaymentOptions bolt12 = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message MakePaymentRequest {
|
message MakePaymentRequest {
|
||||||
MeltQuote melt_quote = 1;
|
OutgoingPaymentVariant payment_options = 1;
|
||||||
optional uint64 partial_amount = 2;
|
optional uint64 partial_amount = 2;
|
||||||
optional uint64 max_fee_amount = 3;
|
optional uint64 max_fee_amount = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MakePaymentResponse {
|
message MakePaymentResponse {
|
||||||
string payment_lookup_id = 1;
|
PaymentIdentifier payment_identifier = 1;
|
||||||
optional string payment_proof = 2;
|
optional string payment_proof = 2;
|
||||||
QuoteState status = 3;
|
QuoteState status = 3;
|
||||||
uint64 total_spent = 4;
|
uint64 total_spent = 4;
|
||||||
@@ -101,22 +166,20 @@ message MakePaymentResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message CheckIncomingPaymentRequest {
|
message CheckIncomingPaymentRequest {
|
||||||
string request_lookup_id = 1;
|
PaymentIdentifier request_identifier = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CheckIncomingPaymentResponse {
|
message CheckIncomingPaymentResponse {
|
||||||
QuoteState status = 1;
|
repeated WaitIncomingPaymentResponse payments = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CheckOutgoingPaymentRequest {
|
message CheckOutgoingPaymentRequest {
|
||||||
string request_lookup_id = 1;
|
PaymentIdentifier request_identifier = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message WaitIncomingPaymentRequest {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
message WaitIncomingPaymentResponse {
|
message WaitIncomingPaymentResponse {
|
||||||
string lookup_id = 1;
|
PaymentIdentifier payment_identifier = 1;
|
||||||
|
uint64 payment_amount = 2;
|
||||||
|
string unit = 3;
|
||||||
|
string payment_id = 4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ use std::str::FromStr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use cdk_common::payment::MintPayment;
|
use cdk_common::payment::{IncomingPaymentOptions, MintPayment};
|
||||||
|
use cdk_common::CurrencyUnit;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::{mpsc, Notify};
|
use tokio::sync::{mpsc, Notify};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
@@ -17,6 +19,7 @@ use tonic::{async_trait, Request, Response, Status};
|
|||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer};
|
use super::cdk_payment_processor_server::{CdkPaymentProcessor, CdkPaymentProcessorServer};
|
||||||
|
use crate::error::Error;
|
||||||
use crate::proto::*;
|
use crate::proto::*;
|
||||||
|
|
||||||
type ResponseStream =
|
type ResponseStream =
|
||||||
@@ -162,7 +165,7 @@ impl Drop for PaymentProcessorServer {
|
|||||||
impl CdkPaymentProcessor for PaymentProcessorServer {
|
impl CdkPaymentProcessor for PaymentProcessorServer {
|
||||||
async fn get_settings(
|
async fn get_settings(
|
||||||
&self,
|
&self,
|
||||||
_request: Request<SettingsRequest>,
|
_request: Request<EmptyRequest>,
|
||||||
) -> Result<Response<SettingsResponse>, Status> {
|
) -> Result<Response<SettingsResponse>, Status> {
|
||||||
let settings: Value = self
|
let settings: Value = self
|
||||||
.inner
|
.inner
|
||||||
@@ -179,18 +182,36 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
&self,
|
&self,
|
||||||
request: Request<CreatePaymentRequest>,
|
request: Request<CreatePaymentRequest>,
|
||||||
) -> Result<Response<CreatePaymentResponse>, Status> {
|
) -> Result<Response<CreatePaymentResponse>, Status> {
|
||||||
let CreatePaymentRequest {
|
let CreatePaymentRequest { unit, options } = request.into_inner();
|
||||||
amount,
|
|
||||||
unit,
|
let unit = CurrencyUnit::from_str(&unit)
|
||||||
description,
|
.map_err(|_| Status::invalid_argument("Invalid currency unit"))?;
|
||||||
unix_expiry,
|
|
||||||
} = request.into_inner();
|
let options = options.ok_or_else(|| Status::invalid_argument("Missing payment options"))?;
|
||||||
|
|
||||||
|
let proto_options = match options
|
||||||
|
.options
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing options"))?
|
||||||
|
{
|
||||||
|
incoming_payment_options::Options::Bolt11(opts) => {
|
||||||
|
IncomingPaymentOptions::Bolt11(cdk_common::payment::Bolt11IncomingPaymentOptions {
|
||||||
|
description: opts.description,
|
||||||
|
amount: opts.amount.into(),
|
||||||
|
unix_expiry: opts.unix_expiry,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
incoming_payment_options::Options::Bolt12(opts) => IncomingPaymentOptions::Bolt12(
|
||||||
|
Box::new(cdk_common::payment::Bolt12IncomingPaymentOptions {
|
||||||
|
description: opts.description,
|
||||||
|
amount: opts.amount.map(Into::into),
|
||||||
|
unix_expiry: opts.unix_expiry,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
let unit =
|
|
||||||
CurrencyUnit::from_str(&unit).map_err(|_| Status::invalid_argument("Invalid unit"))?;
|
|
||||||
let invoice_response = self
|
let invoice_response = self
|
||||||
.inner
|
.inner
|
||||||
.create_incoming_payment_request(amount.into(), &unit, description, unix_expiry)
|
.create_incoming_payment_request(&unit, proto_options)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not create invoice"))?;
|
.map_err(|_| Status::internal("Could not create invoice"))?;
|
||||||
|
|
||||||
@@ -203,21 +224,46 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
) -> Result<Response<PaymentQuoteResponse>, Status> {
|
) -> Result<Response<PaymentQuoteResponse>, Status> {
|
||||||
let request = request.into_inner();
|
let request = request.into_inner();
|
||||||
|
|
||||||
let options: Option<cdk_common::MeltOptions> =
|
let unit = CurrencyUnit::from_str(&request.unit)
|
||||||
request.options.as_ref().map(|options| (*options).into());
|
.map_err(|_| Status::invalid_argument("Invalid currency unit"))?;
|
||||||
|
|
||||||
|
let options = match request.request_type() {
|
||||||
|
OutgoingPaymentRequestType::Bolt11Invoice => {
|
||||||
|
let bolt11: cdk_common::Bolt11Invoice =
|
||||||
|
request.request.parse().map_err(Error::Invoice)?;
|
||||||
|
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt11(Box::new(
|
||||||
|
cdk_common::payment::Bolt11OutgoingPaymentOptions {
|
||||||
|
bolt11,
|
||||||
|
max_fee_amount: None,
|
||||||
|
timeout_secs: None,
|
||||||
|
melt_options: request.options.map(Into::into),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
OutgoingPaymentRequestType::Bolt12Offer => {
|
||||||
|
// Parse offer to verify it's valid, but store as string
|
||||||
|
let _: Offer = request.request.parse().map_err(|_| Error::Bolt12Parse)?;
|
||||||
|
|
||||||
|
cdk_common::payment::OutgoingPaymentOptions::Bolt12(Box::new(
|
||||||
|
cdk_common::payment::Bolt12OutgoingPaymentOptions {
|
||||||
|
offer: Offer::from_str(&request.request).unwrap(),
|
||||||
|
max_fee_amount: None,
|
||||||
|
timeout_secs: None,
|
||||||
|
invoice: None,
|
||||||
|
melt_options: request.options.map(Into::into),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let payment_quote = self
|
let payment_quote = self
|
||||||
.inner
|
.inner
|
||||||
.get_payment_quote(
|
.get_payment_quote(&unit, options)
|
||||||
&request.request,
|
|
||||||
&CurrencyUnit::from_str(&request.unit)
|
|
||||||
.map_err(|_| Status::invalid_argument("Invalid currency unit"))?,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not get bolt11 melt quote: {}", err);
|
tracing::error!("Could not get payment quote: {}", err);
|
||||||
Status::internal("Could not get melt quote")
|
Status::internal("Could not get quote")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Response::new(payment_quote.into()))
|
Ok(Response::new(payment_quote.into()))
|
||||||
@@ -229,17 +275,51 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
) -> Result<Response<MakePaymentResponse>, Status> {
|
) -> Result<Response<MakePaymentResponse>, Status> {
|
||||||
let request = request.into_inner();
|
let request = request.into_inner();
|
||||||
|
|
||||||
let pay_invoice = self
|
let options = request
|
||||||
|
.payment_options
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing payment options"))?;
|
||||||
|
|
||||||
|
let (unit, payment_options) = match options
|
||||||
|
.options
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing options"))?
|
||||||
|
{
|
||||||
|
outgoing_payment_variant::Options::Bolt11(opts) => {
|
||||||
|
let bolt11: cdk_common::Bolt11Invoice =
|
||||||
|
opts.bolt11.parse().map_err(Error::Invoice)?;
|
||||||
|
|
||||||
|
let payment_options = cdk_common::payment::OutgoingPaymentOptions::Bolt11(
|
||||||
|
Box::new(cdk_common::payment::Bolt11OutgoingPaymentOptions {
|
||||||
|
bolt11,
|
||||||
|
max_fee_amount: opts.max_fee_amount.map(Into::into),
|
||||||
|
timeout_secs: opts.timeout_secs,
|
||||||
|
melt_options: opts.melt_options.map(Into::into),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
(CurrencyUnit::Msat, payment_options)
|
||||||
|
}
|
||||||
|
outgoing_payment_variant::Options::Bolt12(opts) => {
|
||||||
|
let offer = Offer::from_str(&opts.offer)
|
||||||
|
.map_err(|_| Error::Bolt12Parse)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let payment_options = cdk_common::payment::OutgoingPaymentOptions::Bolt12(
|
||||||
|
Box::new(cdk_common::payment::Bolt12OutgoingPaymentOptions {
|
||||||
|
offer,
|
||||||
|
max_fee_amount: opts.max_fee_amount.map(Into::into),
|
||||||
|
timeout_secs: opts.timeout_secs,
|
||||||
|
invoice: opts.invoice,
|
||||||
|
melt_options: opts.melt_options.map(Into::into),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
(CurrencyUnit::Msat, payment_options)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pay_response = self
|
||||||
.inner
|
.inner
|
||||||
.make_payment(
|
.make_payment(&unit, payment_options)
|
||||||
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
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
tracing::error!("Could not make payment: {}", err);
|
tracing::error!("Could not make payment: {}", err);
|
||||||
@@ -255,7 +335,7 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Response::new(pay_invoice.into()))
|
Ok(Response::new(pay_response.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_incoming_payment(
|
async fn check_incoming_payment(
|
||||||
@@ -264,14 +344,20 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
) -> Result<Response<CheckIncomingPaymentResponse>, Status> {
|
) -> Result<Response<CheckIncomingPaymentResponse>, Status> {
|
||||||
let request = request.into_inner();
|
let request = request.into_inner();
|
||||||
|
|
||||||
let check_response = self
|
let payment_identifier = request
|
||||||
|
.request_identifier
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing request identifier"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid request identifier"))?;
|
||||||
|
|
||||||
|
let check_responses = self
|
||||||
.inner
|
.inner
|
||||||
.check_incoming_payment_status(&request.request_lookup_id)
|
.check_incoming_payment_status(&payment_identifier)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not check incoming payment status"))?;
|
.map_err(|_| Status::internal("Could not check incoming payment status"))?;
|
||||||
|
|
||||||
Ok(Response::new(CheckIncomingPaymentResponse {
|
Ok(Response::new(CheckIncomingPaymentResponse {
|
||||||
status: QuoteState::from(check_response).into(),
|
payments: check_responses.into_iter().map(|r| r.into()).collect(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,23 +367,28 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
) -> Result<Response<MakePaymentResponse>, Status> {
|
) -> Result<Response<MakePaymentResponse>, Status> {
|
||||||
let request = request.into_inner();
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
let payment_identifier = request
|
||||||
|
.request_identifier
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing request identifier"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid request identifier"))?;
|
||||||
|
|
||||||
let check_response = self
|
let check_response = self
|
||||||
.inner
|
.inner
|
||||||
.check_outgoing_payment(&request.request_lookup_id)
|
.check_outgoing_payment(&payment_identifier)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::internal("Could not check incoming payment status"))?;
|
.map_err(|_| Status::internal("Could not check outgoing payment status"))?;
|
||||||
|
|
||||||
Ok(Response::new(check_response.into()))
|
Ok(Response::new(check_response.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
type WaitIncomingPaymentStream = ResponseStream;
|
type WaitIncomingPaymentStream = ResponseStream;
|
||||||
|
|
||||||
// Clippy thinks select is not stable but it compiles fine on MSRV (1.63.0)
|
|
||||||
#[allow(clippy::incompatible_msrv)]
|
#[allow(clippy::incompatible_msrv)]
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn wait_incoming_payment(
|
async fn wait_incoming_payment(
|
||||||
&self,
|
&self,
|
||||||
_request: Request<WaitIncomingPaymentRequest>,
|
_request: Request<EmptyRequest>,
|
||||||
) -> Result<Response<Self::WaitIncomingPaymentStream>, Status> {
|
) -> Result<Response<Self::WaitIncomingPaymentStream>, Status> {
|
||||||
tracing::debug!("Server waiting for payment stream");
|
tracing::debug!("Server waiting for payment stream");
|
||||||
let (tx, rx) = mpsc::channel(128);
|
let (tx, rx) = mpsc::channel(128);
|
||||||
@@ -308,17 +399,19 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = shutdown_clone.notified() => {
|
_ = shutdown_clone.notified() => {
|
||||||
tracing::info!("Shutdown signal received, stopping task for ");
|
tracing::info!("Shutdown signal received, stopping task");
|
||||||
ln.cancel_wait_invoice();
|
ln.cancel_wait_invoice();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
result = ln.wait_any_incoming_payment() => {
|
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(payment_response) = stream.next().await {
|
||||||
match tx.send(Result::<_, Status>::Ok(WaitIncomingPaymentResponse{lookup_id: request_lookup_id} )).await {
|
match tx.send(Result::<_, Status>::Ok(payment_response.into()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// item (server response) was queued to be send to client
|
// Response was queued to be sent to client
|
||||||
}
|
}
|
||||||
Err(item) => {
|
Err(item) => {
|
||||||
tracing::error!("Error adding incoming payment to stream: {}", item);
|
tracing::error!("Error adding incoming payment to stream: {}", item);
|
||||||
@@ -328,8 +421,7 @@ impl CdkPaymentProcessor for PaymentProcessorServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("Could not get invoice stream for {}", err);
|
tracing::warn!("Could not get invoice stream: {}", err);
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ pub enum Error {
|
|||||||
/// Invalid keyset ID
|
/// Invalid keyset ID
|
||||||
#[error("Invalid keyset ID")]
|
#[error("Invalid keyset ID")]
|
||||||
InvalidKeysetId,
|
InvalidKeysetId,
|
||||||
|
/// Invalid melt payment request
|
||||||
|
#[error("Invalid melt payment request")]
|
||||||
|
InvalidMeltPaymentRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for cdk_common::database::Error {
|
impl From<Error> for cdk_common::database::Error {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub async fn new_with_state(
|
|||||||
let mut tx = MintDatabase::begin_transaction(&db).await?;
|
let mut tx = MintDatabase::begin_transaction(&db).await?;
|
||||||
|
|
||||||
for quote in mint_quotes {
|
for quote in mint_quotes {
|
||||||
tx.add_or_replace_mint_quote(quote).await?;
|
tx.add_mint_quote(quote).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for quote in melt_quotes {
|
for quote in melt_quotes {
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
|
|||||||
("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/20250406093755_mint_created_time_signature.sql"#)),
|
("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/20250406093755_mint_created_time_signature.sql"#)),
|
||||||
("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/20250415093121_drop_keystore_foreign.sql"#)),
|
("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/20250415093121_drop_keystore_foreign.sql"#)),
|
||||||
("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/20250626120251_rename_blind_message_y_to_b.sql"#)),
|
("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/20250626120251_rename_blind_message_y_to_b.sql"#)),
|
||||||
|
("20250706101057_bolt12.sql", include_str!(r#"./migrations/20250706101057_bolt12.sql"#)),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
-- Add new columns to mint_quote table
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN amount_paid INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN amount_issued INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'BOLT11';
|
||||||
|
ALTER TABLE mint_quote DROP COLUMN issued_time;
|
||||||
|
ALTER TABLE mint_quote DROP COLUMN paid_time;
|
||||||
|
|
||||||
|
-- Set amount_paid equal to amount for quotes with PAID or ISSUED state
|
||||||
|
UPDATE mint_quote SET amount_paid = amount WHERE state = 'PAID' OR state = 'ISSUED';
|
||||||
|
|
||||||
|
-- Set amount_issued equal to amount for quotes with ISSUED state
|
||||||
|
UPDATE mint_quote SET amount_issued = amount WHERE state = 'ISSUED';
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS mint_quote_state_index;
|
||||||
|
|
||||||
|
-- Remove the state column from mint_quote table
|
||||||
|
ALTER TABLE mint_quote DROP COLUMN state;
|
||||||
|
|
||||||
|
-- Remove NOT NULL constraint from amount column
|
||||||
|
CREATE TABLE mint_quote_temp (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
amount INTEGER,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
request TEXT NOT NULL,
|
||||||
|
expiry INTEGER NOT NULL,
|
||||||
|
request_lookup_id TEXT UNIQUE,
|
||||||
|
pubkey TEXT,
|
||||||
|
created_time INTEGER NOT NULL DEFAULT 0,
|
||||||
|
amount_paid INTEGER NOT NULL DEFAULT 0,
|
||||||
|
amount_issued INTEGER NOT NULL DEFAULT 0,
|
||||||
|
payment_method TEXT NOT NULL DEFAULT 'BOLT11'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO mint_quote_temp (id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method)
|
||||||
|
SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method
|
||||||
|
FROM mint_quote;
|
||||||
|
|
||||||
|
DROP TABLE mint_quote;
|
||||||
|
ALTER TABLE mint_quote_temp RENAME TO mint_quote;
|
||||||
|
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN request_lookup_id_kind TEXT NOT NULL DEFAULT 'payment_hash';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_quote_created_time ON mint_quote(created_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_quote_expiry ON mint_quote(expiry);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_quote_request_lookup_id ON mint_quote(request_lookup_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mint_quote_request_lookup_id_and_kind ON mint_quote(request_lookup_id, request_lookup_id_kind);
|
||||||
|
|
||||||
|
-- Create mint_quote_payments table
|
||||||
|
CREATE TABLE mint_quote_payments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
quote_id TEXT NOT NULL,
|
||||||
|
payment_id TEXT NOT NULL UNIQUE,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (quote_id) REFERENCES mint_quote(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index on payment_id for faster lookups
|
||||||
|
CREATE INDEX idx_mint_quote_payments_payment_id ON mint_quote_payments(payment_id);
|
||||||
|
CREATE INDEX idx_mint_quote_payments_quote_id ON mint_quote_payments(quote_id);
|
||||||
|
|
||||||
|
-- Create mint_quote_issued table
|
||||||
|
CREATE TABLE mint_quote_issued (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
quote_id TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (quote_id) REFERENCES mint_quote(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index on quote_id for faster lookups
|
||||||
|
CREATE INDEX idx_mint_quote_issued_quote_id ON mint_quote_issued(quote_id);
|
||||||
|
|
||||||
|
-- Add new columns to melt_quote table
|
||||||
|
ALTER TABLE melt_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'bolt11';
|
||||||
|
ALTER TABLE melt_quote ADD COLUMN options TEXT;
|
||||||
|
ALTER TABLE melt_quote ADD COLUMN request_lookup_id_kind TEXT NOT NULL DEFAULT 'payment_hash';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_melt_quote_request_lookup_id_and_kind ON mint_quote(request_lookup_id, request_lookup_id_kind);
|
||||||
|
|
||||||
|
ALTER TABLE melt_quote DROP COLUMN msat_to_pay;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,4 +17,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
|
|||||||
("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
|
("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
|
||||||
("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
|
("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
|
||||||
("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
|
("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
|
||||||
|
("20250707093445_bolt12.sql", include_str!(r#"./migrations/20250707093445_bolt12.sql"#)),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
ALTER TABLE mint_quote ADD COLUMN amount_paid INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN amount_minted INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE mint_quote ADD COLUMN payment_method TEXT NOT NULL DEFAULT 'BOLT11';
|
||||||
|
|
||||||
|
-- Remove NOT NULL constraint from amount column
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
CREATE TABLE mint_quote_new (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
mint_url TEXT NOT NULL,
|
||||||
|
payment_method TEXT NOT NULL DEFAULT 'bolt11',
|
||||||
|
amount INTEGER,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
request TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
expiry INTEGER NOT NULL,
|
||||||
|
amount_paid INTEGER NOT NULL DEFAULT 0,
|
||||||
|
amount_issued INTEGER NOT NULL DEFAULT 0,
|
||||||
|
secret_key TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Explicitly specify columns for proper mapping
|
||||||
|
INSERT INTO mint_quote_new (
|
||||||
|
id,
|
||||||
|
mint_url,
|
||||||
|
payment_method,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
request,
|
||||||
|
state,
|
||||||
|
expiry,
|
||||||
|
amount_paid,
|
||||||
|
amount_issued,
|
||||||
|
secret_key
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mint_url,
|
||||||
|
'bolt11', -- Default value for the new payment_method column
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
request,
|
||||||
|
state,
|
||||||
|
expiry,
|
||||||
|
0, -- Default value for amount_paid
|
||||||
|
0, -- Default value for amount_minted
|
||||||
|
secret_key
|
||||||
|
FROM mint_quote;
|
||||||
|
|
||||||
|
DROP TABLE mint_quote;
|
||||||
|
ALTER TABLE mint_quote_new RENAME TO mint_quote;
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
|
|
||||||
|
|
||||||
|
-- Set amount_paid equal to amount for quotes with PAID or ISSUED state
|
||||||
|
UPDATE mint_quote SET amount_paid = amount WHERE state = 'PAID' OR state = 'ISSUED';
|
||||||
|
|
||||||
|
-- Set amount_issued equal to amount for quotes with ISSUED state
|
||||||
|
UPDATE mint_quote SET amount_issued = amount WHERE state = 'ISSUED';
|
||||||
@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
|
|||||||
use cdk_common::secret::Secret;
|
use cdk_common::secret::Secret;
|
||||||
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
|
||||||
use cdk_common::{
|
use cdk_common::{
|
||||||
database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
|
database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof,
|
||||||
PublicKey, SecretKey, SpendingConditions, State,
|
ProofDleq, PublicKey, SecretKey, SpendingConditions, State,
|
||||||
};
|
};
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -376,9 +376,9 @@ ON CONFLICT(mint_url) DO UPDATE SET
|
|||||||
Statement::new(
|
Statement::new(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO mint_quote
|
INSERT INTO mint_quote
|
||||||
(id, mint_url, amount, unit, request, state, expiry, secret_key)
|
(id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid)
|
||||||
VALUES
|
VALUES
|
||||||
(:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key)
|
(:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
mint_url = excluded.mint_url,
|
mint_url = excluded.mint_url,
|
||||||
amount = excluded.amount,
|
amount = excluded.amount,
|
||||||
@@ -386,18 +386,24 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
request = excluded.request,
|
request = excluded.request,
|
||||||
state = excluded.state,
|
state = excluded.state,
|
||||||
expiry = excluded.expiry,
|
expiry = excluded.expiry,
|
||||||
secret_key = excluded.secret_key
|
secret_key = excluded.secret_key,
|
||||||
|
payment_method = excluded.payment_method,
|
||||||
|
amount_issued = excluded.amount_issued,
|
||||||
|
amount_paid = excluded.amount_paid
|
||||||
;
|
;
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(":id", quote.id.to_string())
|
.bind(":id", quote.id.to_string())
|
||||||
.bind(":mint_url", quote.mint_url.to_string())
|
.bind(":mint_url", quote.mint_url.to_string())
|
||||||
.bind(":amount", u64::from(quote.amount) as i64)
|
.bind(":amount", quote.amount.map(|a| a.to_i64()))
|
||||||
.bind(":unit", quote.unit.to_string())
|
.bind(":unit", quote.unit.to_string())
|
||||||
.bind(":request", quote.request)
|
.bind(":request", quote.request)
|
||||||
.bind(":state", quote.state.to_string())
|
.bind(":state", quote.state.to_string())
|
||||||
.bind(":expiry", quote.expiry as i64)
|
.bind(":expiry", quote.expiry as i64)
|
||||||
.bind(":secret_key", quote.secret_key.map(|p| p.to_string()))
|
.bind(":secret_key", quote.secret_key.map(|p| p.to_string()))
|
||||||
|
.bind(":payment_method", quote.payment_method.to_string())
|
||||||
|
.bind(":amount_issued", quote.amount_issued.to_i64())
|
||||||
|
.bind(":amount_paid", quote.amount_paid.to_i64())
|
||||||
.execute(&self.pool.get().map_err(Error::Pool)?)
|
.execute(&self.pool.get().map_err(Error::Pool)?)
|
||||||
.map_err(Error::Sqlite)?;
|
.map_err(Error::Sqlite)?;
|
||||||
|
|
||||||
@@ -416,7 +422,10 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
request,
|
request,
|
||||||
state,
|
state,
|
||||||
expiry,
|
expiry,
|
||||||
secret_key
|
secret_key,
|
||||||
|
payment_method,
|
||||||
|
amount_issued,
|
||||||
|
amount_paid
|
||||||
FROM
|
FROM
|
||||||
mint_quote
|
mint_quote
|
||||||
WHERE
|
WHERE
|
||||||
@@ -950,16 +959,24 @@ fn sqlite_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
|
|||||||
request,
|
request,
|
||||||
state,
|
state,
|
||||||
expiry,
|
expiry,
|
||||||
secret_key
|
secret_key,
|
||||||
|
row_method,
|
||||||
|
row_amount_minted,
|
||||||
|
row_amount_paid
|
||||||
) = row
|
) = row
|
||||||
);
|
);
|
||||||
|
|
||||||
let amount: u64 = column_as_number!(amount);
|
let amount: Option<i64> = column_as_nullable_number!(amount);
|
||||||
|
|
||||||
|
let amount_paid: u64 = column_as_number!(row_amount_paid);
|
||||||
|
let amount_minted: u64 = column_as_number!(row_amount_minted);
|
||||||
|
let payment_method =
|
||||||
|
PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
|
||||||
|
|
||||||
Ok(MintQuote {
|
Ok(MintQuote {
|
||||||
id: column_as_string!(id),
|
id: column_as_string!(id),
|
||||||
mint_url: column_as_string!(mint_url, MintUrl::from_str),
|
mint_url: column_as_string!(mint_url, MintUrl::from_str),
|
||||||
amount: Amount::from(amount),
|
amount: amount.and_then(Amount::from_i64),
|
||||||
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
unit: column_as_string!(unit, CurrencyUnit::from_str),
|
||||||
request: column_as_string!(request),
|
request: column_as_string!(request),
|
||||||
state: column_as_string!(state, MintQuoteState::from_str),
|
state: column_as_string!(state, MintQuoteState::from_str),
|
||||||
@@ -967,6 +984,9 @@ fn sqlite_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
|
|||||||
secret_key: column_as_nullable_string!(secret_key)
|
secret_key: column_as_nullable_string!(secret_key)
|
||||||
.map(|v| SecretKey::from_str(&v))
|
.map(|v| SecretKey::from_str(&v))
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
payment_method,
|
||||||
|
amount_issued: amount_minted.into(),
|
||||||
|
amount_paid: amount_paid.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ async-trait.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
bitcoin.workspace = true
|
bitcoin.workspace = true
|
||||||
ciborium.workspace = true
|
ciborium.workspace = true
|
||||||
|
lightning.workspace = true
|
||||||
lightning-invoice.workspace = true
|
lightning-invoice.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, optional = true }
|
reqwest = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ impl MintBuilder {
|
|||||||
self.mint_info.nuts.nut15 = mpp;
|
self.mint_info.nuts.nut15 = mpp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == PaymentMethod::Bolt11 {
|
|
||||||
let mint_method_settings = MintMethodSettings {
|
let mint_method_settings = MintMethodSettings {
|
||||||
method: method.clone(),
|
method: method.clone(),
|
||||||
unit: unit.clone(),
|
unit: unit.clone(),
|
||||||
@@ -235,7 +234,6 @@ impl MintBuilder {
|
|||||||
};
|
};
|
||||||
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
|
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
|
||||||
self.mint_info.nuts.nut05.disabled = false;
|
self.mint_info.nuts.nut05.disabled = false;
|
||||||
}
|
|
||||||
|
|
||||||
ln.insert(ln_key.clone(), ln_backend);
|
ln.insert(ln_key.clone(), ln_backend);
|
||||||
|
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
use cdk_common::payment::Bolt11Settings;
|
|
||||||
use tracing::instrument;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::mint::{
|
|
||||||
CurrencyUnit, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
|
|
||||||
MintRequest, MintResponse, NotificationPayload, PublicKey, Verification,
|
|
||||||
};
|
|
||||||
use crate::nuts::PaymentMethod;
|
|
||||||
use crate::util::unix_time;
|
|
||||||
use crate::{ensure_cdk, Amount, Error, Mint};
|
|
||||||
|
|
||||||
impl Mint {
|
|
||||||
/// Checks that minting is enabled, request is supported unit and within range
|
|
||||||
async fn check_mint_request_acceptable(
|
|
||||||
&self,
|
|
||||||
amount: Amount,
|
|
||||||
unit: &CurrencyUnit,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mint_info = self.localstore.get_mint_info().await?;
|
|
||||||
let nut04 = &mint_info.nuts.nut04;
|
|
||||||
|
|
||||||
ensure_cdk!(!nut04.disabled, Error::MintingDisabled);
|
|
||||||
|
|
||||||
let settings = nut04
|
|
||||||
.get_settings(unit, &PaymentMethod::Bolt11)
|
|
||||||
.ok_or(Error::UnsupportedUnit)?;
|
|
||||||
|
|
||||||
let is_above_max = settings
|
|
||||||
.max_amount
|
|
||||||
.is_some_and(|max_amount| amount > max_amount);
|
|
||||||
let is_below_min = settings
|
|
||||||
.min_amount
|
|
||||||
.is_some_and(|min_amount| amount < min_amount);
|
|
||||||
let is_out_of_range = is_above_max || is_below_min;
|
|
||||||
|
|
||||||
ensure_cdk!(
|
|
||||||
!is_out_of_range,
|
|
||||||
Error::AmountOutofLimitRange(
|
|
||||||
settings.min_amount.unwrap_or_default(),
|
|
||||||
settings.max_amount.unwrap_or_default(),
|
|
||||||
amount,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new mint bolt11 quote
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn get_mint_bolt11_quote(
|
|
||||||
&self,
|
|
||||||
mint_quote_request: MintQuoteBolt11Request,
|
|
||||||
) -> Result<MintQuoteBolt11Response<Uuid>, Error> {
|
|
||||||
let MintQuoteBolt11Request {
|
|
||||||
amount,
|
|
||||||
unit,
|
|
||||||
description,
|
|
||||||
pubkey,
|
|
||||||
} = mint_quote_request;
|
|
||||||
|
|
||||||
self.check_mint_request_acceptable(amount, &unit).await?;
|
|
||||||
|
|
||||||
let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
|
|
||||||
|
|
||||||
let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
|
|
||||||
|
|
||||||
let quote_expiry = unix_time() + mint_ttl;
|
|
||||||
|
|
||||||
let settings = ln.get_settings().await?;
|
|
||||||
let settings: Bolt11Settings = serde_json::from_value(settings)?;
|
|
||||||
|
|
||||||
if description.is_some() && !settings.invoice_description {
|
|
||||||
tracing::error!("Backend does not support invoice description");
|
|
||||||
return Err(Error::InvoiceDescriptionUnsupported);
|
|
||||||
}
|
|
||||||
|
|
||||||
let create_invoice_response = ln
|
|
||||||
.create_incoming_payment_request(
|
|
||||||
amount,
|
|
||||||
&unit,
|
|
||||||
description.unwrap_or("".to_string()),
|
|
||||||
Some(quote_expiry),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
tracing::error!("Could not create invoice: {}", err);
|
|
||||||
Error::InvalidPaymentRequest
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let quote = MintQuote::new(
|
|
||||||
create_invoice_response.request.to_string(),
|
|
||||||
unit.clone(),
|
|
||||||
amount,
|
|
||||||
create_invoice_response.expiry.unwrap_or(0),
|
|
||||||
create_invoice_response.request_lookup_id.clone(),
|
|
||||||
pubkey,
|
|
||||||
);
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
"New mint quote {} for {} {} with request id {}",
|
|
||||||
quote.id,
|
|
||||||
amount,
|
|
||||||
unit,
|
|
||||||
create_invoice_response.request_lookup_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
|
||||||
tx.add_or_replace_mint_quote(quote.clone()).await?;
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
let quote: MintQuoteBolt11Response<Uuid> = quote.into();
|
|
||||||
|
|
||||||
self.pubsub_manager
|
|
||||||
.broadcast(NotificationPayload::MintQuoteBolt11Response(quote.clone()));
|
|
||||||
|
|
||||||
Ok(quote)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check mint quote
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn check_mint_quote(
|
|
||||||
&self,
|
|
||||||
quote_id: &Uuid,
|
|
||||||
) -> Result<MintQuoteBolt11Response<Uuid>, Error> {
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
|
||||||
let mut mint_quote = tx
|
|
||||||
.get_mint_quote(quote_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::UnknownQuote)?;
|
|
||||||
|
|
||||||
// Since the pending state is not part of the NUT it should not be part of the
|
|
||||||
// response. In practice the wallet should not be checking the state of
|
|
||||||
// a quote while waiting for the mint response.
|
|
||||||
if mint_quote.state == MintQuoteState::Unpaid {
|
|
||||||
self.check_mint_quote_paid(tx, &mut mint_quote)
|
|
||||||
.await?
|
|
||||||
.commit()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MintQuoteBolt11Response {
|
|
||||||
quote: mint_quote.id,
|
|
||||||
request: mint_quote.request,
|
|
||||||
state: mint_quote.state,
|
|
||||||
expiry: Some(mint_quote.expiry),
|
|
||||||
pubkey: mint_quote.pubkey,
|
|
||||||
amount: Some(mint_quote.amount),
|
|
||||||
unit: Some(mint_quote.unit.clone()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get mint quotes
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
|
|
||||||
let quotes = self.localstore.get_mint_quotes().await?;
|
|
||||||
Ok(quotes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove mint quote
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
|
||||||
tx.remove_mint_quote(quote_id).await?;
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flag mint quote as paid
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn pay_mint_quote_for_request_id(
|
|
||||||
&self,
|
|
||||||
request_lookup_id: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if let Ok(Some(mint_quote)) = self
|
|
||||||
.localstore
|
|
||||||
.get_mint_quote_by_request_lookup_id(request_lookup_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
self.pay_mint_quote(&mint_quote).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark mint quote as paid
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn pay_mint_quote(&self, mint_quote: &MintQuote) -> Result<(), Error> {
|
|
||||||
tracing::debug!(
|
|
||||||
"Received payment notification for mint quote {}",
|
|
||||||
mint_quote.id
|
|
||||||
);
|
|
||||||
if mint_quote.state != MintQuoteState::Issued && mint_quote.state != MintQuoteState::Paid {
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
|
||||||
tx.update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid)
|
|
||||||
.await?;
|
|
||||||
tx.commit().await?;
|
|
||||||
} else {
|
|
||||||
tracing::debug!(
|
|
||||||
"{} Quote already {} continuing",
|
|
||||||
mint_quote.id,
|
|
||||||
mint_quote.state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pubsub_manager
|
|
||||||
.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process mint request
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn process_mint_request(
|
|
||||||
&self,
|
|
||||||
mint_request: MintRequest<Uuid>,
|
|
||||||
) -> Result<MintResponse, Error> {
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
|
||||||
|
|
||||||
let mut mint_quote = tx
|
|
||||||
.get_mint_quote(&mint_request.quote)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::UnknownQuote)?;
|
|
||||||
|
|
||||||
let mut tx = if mint_quote.state == MintQuoteState::Unpaid {
|
|
||||||
self.check_mint_quote_paid(tx, &mut mint_quote).await?
|
|
||||||
} else {
|
|
||||||
tx
|
|
||||||
};
|
|
||||||
|
|
||||||
match mint_quote.state {
|
|
||||||
MintQuoteState::Unpaid => {
|
|
||||||
return Err(Error::UnpaidQuote);
|
|
||||||
}
|
|
||||||
MintQuoteState::Pending => {
|
|
||||||
return Err(Error::PendingQuote);
|
|
||||||
}
|
|
||||||
MintQuoteState::Issued => {
|
|
||||||
return Err(Error::IssuedQuote);
|
|
||||||
}
|
|
||||||
MintQuoteState::Paid => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the there is a public key provoided in mint quote request
|
|
||||||
// verify the signature is provided for the mint request
|
|
||||||
if let Some(pubkey) = mint_quote.pubkey {
|
|
||||||
mint_request.verify_signature(pubkey)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Verification { amount, unit } =
|
|
||||||
match self.verify_outputs(&mut tx, &mint_request.outputs).await {
|
|
||||||
Ok(verification) => verification,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::debug!("Could not verify mint outputs");
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// We check the total value of blinded messages == mint quote
|
|
||||||
if amount != mint_quote.amount {
|
|
||||||
return Err(Error::TransactionUnbalanced(
|
|
||||||
mint_quote.amount.into(),
|
|
||||||
mint_request.total_amount()?.into(),
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let unit = unit.ok_or(Error::UnsupportedUnit)?;
|
|
||||||
ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
|
|
||||||
|
|
||||||
let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
|
|
||||||
|
|
||||||
for blinded_message in mint_request.outputs.iter() {
|
|
||||||
let blind_signature = self.blind_sign(blinded_message.clone()).await?;
|
|
||||||
blind_signatures.push(blind_signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.add_blind_signatures(
|
|
||||||
&mint_request
|
|
||||||
.outputs
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.blinded_secret)
|
|
||||||
.collect::<Vec<PublicKey>>(),
|
|
||||||
&blind_signatures,
|
|
||||||
Some(mint_request.quote),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tx.update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
self.pubsub_manager
|
|
||||||
.mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
|
|
||||||
|
|
||||||
Ok(MintResponse {
|
|
||||||
signatures: blind_signatures,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,601 @@
|
|||||||
|
use cdk_common::mint::MintQuote;
|
||||||
|
use cdk_common::payment::{
|
||||||
|
Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
|
||||||
|
IncomingPaymentOptions, WaitPaymentResponse,
|
||||||
|
};
|
||||||
|
use cdk_common::util::unix_time;
|
||||||
|
use cdk_common::{
|
||||||
|
database, ensure_cdk, Amount, CurrencyUnit, Error, MintQuoteBolt11Request,
|
||||||
|
MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
|
||||||
|
MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
|
||||||
|
};
|
||||||
|
use tracing::instrument;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::mint::Verification;
|
||||||
|
use crate::Mint;
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
mod auth;
|
mod auth;
|
||||||
mod issue_nut04;
|
|
||||||
|
/// Request for creating a mint quote
|
||||||
|
///
|
||||||
|
/// This enum represents the different types of payment requests that can be used
|
||||||
|
/// to create a mint quote.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum MintQuoteRequest {
|
||||||
|
/// Lightning Network BOLT11 invoice request
|
||||||
|
Bolt11(MintQuoteBolt11Request),
|
||||||
|
/// Lightning Network BOLT12 offer request
|
||||||
|
Bolt12(MintQuoteBolt12Request),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MintQuoteBolt11Request> for MintQuoteRequest {
|
||||||
|
fn from(request: MintQuoteBolt11Request) -> Self {
|
||||||
|
MintQuoteRequest::Bolt11(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MintQuoteBolt12Request> for MintQuoteRequest {
|
||||||
|
fn from(request: MintQuoteBolt12Request) -> Self {
|
||||||
|
MintQuoteRequest::Bolt12(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response for a mint quote request
|
||||||
|
///
|
||||||
|
/// This enum represents the different types of payment responses that can be returned
|
||||||
|
/// when creating a mint quote.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum MintQuoteResponse {
|
||||||
|
/// Lightning Network BOLT11 invoice response
|
||||||
|
Bolt11(MintQuoteBolt11Response<Uuid>),
|
||||||
|
/// Lightning Network BOLT12 offer response
|
||||||
|
Bolt12(MintQuoteBolt12Response<Uuid>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MintQuoteResponse> for MintQuoteBolt11Response<Uuid> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
|
||||||
|
match response {
|
||||||
|
MintQuoteResponse::Bolt11(bolt11_response) => Ok(bolt11_response),
|
||||||
|
_ => Err(Error::InvalidPaymentMethod),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MintQuoteResponse> for MintQuoteBolt12Response<Uuid> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
|
||||||
|
match response {
|
||||||
|
MintQuoteResponse::Bolt12(bolt12_response) => Ok(bolt12_response),
|
||||||
|
_ => Err(Error::InvalidPaymentMethod),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MintQuote> for MintQuoteResponse {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
|
||||||
|
match quote.payment_method {
|
||||||
|
PaymentMethod::Bolt11 => {
|
||||||
|
let bolt11_response: MintQuoteBolt11Response<Uuid> = quote.into();
|
||||||
|
Ok(MintQuoteResponse::Bolt11(bolt11_response))
|
||||||
|
}
|
||||||
|
PaymentMethod::Bolt12 => {
|
||||||
|
let bolt12_response = MintQuoteBolt12Response::try_from(quote)?;
|
||||||
|
Ok(MintQuoteResponse::Bolt12(bolt12_response))
|
||||||
|
}
|
||||||
|
PaymentMethod::Custom(_) => Err(Error::InvalidPaymentMethod),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MintQuoteResponse> for MintQuoteBolt11Response<String> {
|
||||||
|
fn from(response: MintQuoteResponse) -> Self {
|
||||||
|
match response {
|
||||||
|
MintQuoteResponse::Bolt11(bolt11_response) => MintQuoteBolt11Response {
|
||||||
|
quote: bolt11_response.quote.to_string(),
|
||||||
|
state: bolt11_response.state,
|
||||||
|
request: bolt11_response.request,
|
||||||
|
expiry: bolt11_response.expiry,
|
||||||
|
pubkey: bolt11_response.pubkey,
|
||||||
|
amount: bolt11_response.amount,
|
||||||
|
unit: bolt11_response.unit,
|
||||||
|
},
|
||||||
|
_ => panic!("Expected Bolt11 response"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mint {
|
||||||
|
/// Validates that a mint request meets all requirements
|
||||||
|
///
|
||||||
|
/// Checks that:
|
||||||
|
/// - Minting is enabled for the requested payment method
|
||||||
|
/// - The currency unit is supported
|
||||||
|
/// - The amount (if provided) is within the allowed range for the payment method
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `amount` - Optional amount to validate
|
||||||
|
/// * `unit` - Currency unit for the request
|
||||||
|
/// * `payment_method` - Payment method (Bolt11, Bolt12, etc.)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` if the request is acceptable
|
||||||
|
/// * `Error` if any validation fails
|
||||||
|
pub async fn check_mint_request_acceptable(
|
||||||
|
&self,
|
||||||
|
amount: Option<Amount>,
|
||||||
|
unit: &CurrencyUnit,
|
||||||
|
payment_method: &PaymentMethod,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mint_info = self.localstore.get_mint_info().await?;
|
||||||
|
|
||||||
|
let nut04 = &mint_info.nuts.nut04;
|
||||||
|
ensure_cdk!(!nut04.disabled, Error::MintingDisabled);
|
||||||
|
|
||||||
|
let disabled = nut04.disabled;
|
||||||
|
|
||||||
|
ensure_cdk!(!disabled, Error::MintingDisabled);
|
||||||
|
|
||||||
|
let settings = nut04
|
||||||
|
.get_settings(unit, payment_method)
|
||||||
|
.ok_or(Error::UnsupportedUnit)?;
|
||||||
|
|
||||||
|
let min_amount = settings.min_amount;
|
||||||
|
let max_amount = settings.max_amount;
|
||||||
|
|
||||||
|
// Check amount limits if an amount is provided
|
||||||
|
if let Some(amount) = amount {
|
||||||
|
let is_above_max = max_amount.is_some_and(|max_amount| amount > max_amount);
|
||||||
|
let is_below_min = min_amount.is_some_and(|min_amount| amount < min_amount);
|
||||||
|
let is_out_of_range = is_above_max || is_below_min;
|
||||||
|
|
||||||
|
ensure_cdk!(
|
||||||
|
!is_out_of_range,
|
||||||
|
Error::AmountOutofLimitRange(
|
||||||
|
min_amount.unwrap_or_default(),
|
||||||
|
max_amount.unwrap_or_default(),
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new mint quote for the specified payment request
|
||||||
|
///
|
||||||
|
/// Handles both Bolt11 and Bolt12 payment requests by:
|
||||||
|
/// 1. Validating the request parameters
|
||||||
|
/// 2. Creating an appropriate payment request via the payment processor
|
||||||
|
/// 3. Storing the quote in the database
|
||||||
|
/// 4. Broadcasting a notification about the new quote
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `mint_quote_request` - The request containing payment details
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `MintQuoteResponse` - Response with payment details if successful
|
||||||
|
/// * `Error` - If the request is invalid or payment creation fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn get_mint_quote(
|
||||||
|
&self,
|
||||||
|
mint_quote_request: MintQuoteRequest,
|
||||||
|
) -> Result<MintQuoteResponse, Error> {
|
||||||
|
let unit: CurrencyUnit;
|
||||||
|
let amount;
|
||||||
|
let pubkey;
|
||||||
|
let payment_method;
|
||||||
|
|
||||||
|
let create_invoice_response = match mint_quote_request {
|
||||||
|
MintQuoteRequest::Bolt11(bolt11_request) => {
|
||||||
|
unit = bolt11_request.unit;
|
||||||
|
amount = Some(bolt11_request.amount);
|
||||||
|
pubkey = bolt11_request.pubkey;
|
||||||
|
payment_method = PaymentMethod::Bolt11;
|
||||||
|
|
||||||
|
self.check_mint_request_acceptable(
|
||||||
|
Some(bolt11_request.amount),
|
||||||
|
&unit,
|
||||||
|
&payment_method,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
|
||||||
|
|
||||||
|
let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
|
||||||
|
|
||||||
|
let quote_expiry = unix_time() + mint_ttl;
|
||||||
|
|
||||||
|
let settings = ln.get_settings().await?;
|
||||||
|
let settings: Bolt11Settings = serde_json::from_value(settings)?;
|
||||||
|
|
||||||
|
let description = bolt11_request.description;
|
||||||
|
|
||||||
|
if description.is_some() && !settings.invoice_description {
|
||||||
|
tracing::error!("Backend does not support invoice description");
|
||||||
|
return Err(Error::InvoiceDescriptionUnsupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bolt11_options = Bolt11IncomingPaymentOptions {
|
||||||
|
description,
|
||||||
|
amount: bolt11_request.amount,
|
||||||
|
unix_expiry: Some(quote_expiry),
|
||||||
|
};
|
||||||
|
|
||||||
|
let incoming_options = IncomingPaymentOptions::Bolt11(bolt11_options);
|
||||||
|
|
||||||
|
ln.create_incoming_payment_request(&unit, incoming_options)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Could not create invoice: {}", err);
|
||||||
|
Error::InvalidPaymentRequest
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
MintQuoteRequest::Bolt12(bolt12_request) => {
|
||||||
|
unit = bolt12_request.unit;
|
||||||
|
amount = bolt12_request.amount;
|
||||||
|
pubkey = Some(bolt12_request.pubkey);
|
||||||
|
payment_method = PaymentMethod::Bolt12;
|
||||||
|
|
||||||
|
self.check_mint_request_acceptable(amount, &unit, &payment_method)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let ln = self.get_payment_processor(unit.clone(), payment_method.clone())?;
|
||||||
|
|
||||||
|
let description = bolt12_request.description;
|
||||||
|
|
||||||
|
let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
|
||||||
|
|
||||||
|
let expiry = unix_time() + mint_ttl;
|
||||||
|
|
||||||
|
let bolt12_options = Bolt12IncomingPaymentOptions {
|
||||||
|
description,
|
||||||
|
amount,
|
||||||
|
unix_expiry: Some(expiry),
|
||||||
|
};
|
||||||
|
|
||||||
|
let incoming_options = IncomingPaymentOptions::Bolt12(Box::new(bolt12_options));
|
||||||
|
|
||||||
|
ln.create_incoming_payment_request(&unit, incoming_options)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Could not create invoice: {}", err);
|
||||||
|
Error::InvalidPaymentRequest
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote = MintQuote::new(
|
||||||
|
None,
|
||||||
|
create_invoice_response.request.to_string(),
|
||||||
|
unit.clone(),
|
||||||
|
amount,
|
||||||
|
create_invoice_response.expiry.unwrap_or(0),
|
||||||
|
create_invoice_response.request_lookup_id.clone(),
|
||||||
|
pubkey,
|
||||||
|
Amount::ZERO,
|
||||||
|
Amount::ZERO,
|
||||||
|
payment_method.clone(),
|
||||||
|
unix_time(),
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"New {} mint quote {} for {:?} {} with request id {:?}",
|
||||||
|
payment_method,
|
||||||
|
quote.id,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
create_invoice_response.request_lookup_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
tx.add_mint_quote(quote.clone()).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
match payment_method {
|
||||||
|
PaymentMethod::Bolt11 => {
|
||||||
|
let res: MintQuoteBolt11Response<Uuid> = quote.clone().into();
|
||||||
|
self.pubsub_manager
|
||||||
|
.broadcast(NotificationPayload::MintQuoteBolt11Response(res));
|
||||||
|
}
|
||||||
|
PaymentMethod::Bolt12 => {
|
||||||
|
let res: MintQuoteBolt12Response<Uuid> = quote.clone().try_into()?;
|
||||||
|
self.pubsub_manager
|
||||||
|
.broadcast(NotificationPayload::MintQuoteBolt12Response(res));
|
||||||
|
}
|
||||||
|
PaymentMethod::Custom(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
quote.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves all mint quotes from the database
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Vec<MintQuote>` - List of all mint quotes
|
||||||
|
/// * `Error` if database access fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
|
||||||
|
let quotes = self.localstore.get_mint_quotes().await?;
|
||||||
|
Ok(quotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a mint quote from the database
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `quote_id` - The UUID of the quote to remove
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` if removal was successful
|
||||||
|
/// * `Error` if the quote doesn't exist or removal fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
tx.remove_mint_quote(quote_id).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks a mint quote as paid based on the payment request ID
|
||||||
|
///
|
||||||
|
/// Looks up the mint quote by the payment request ID and marks it as paid
|
||||||
|
/// if found.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `wait_payment_response` - Payment response containing payment details
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` if the quote was found and updated
|
||||||
|
/// * `Error` if the update fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn pay_mint_quote_for_request_id(
|
||||||
|
&self,
|
||||||
|
wait_payment_response: WaitPaymentResponse,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if wait_payment_response.payment_amount == Amount::ZERO {
|
||||||
|
tracing::warn!(
|
||||||
|
"Received payment response with 0 amount with payment id {}.",
|
||||||
|
wait_payment_response.payment_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Error::AmountUndefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
|
||||||
|
if let Ok(Some(mint_quote)) = tx
|
||||||
|
.get_mint_quote_by_request_lookup_id(&wait_payment_response.payment_identifier)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.pay_mint_quote(&mut tx, &mint_quote, wait_payment_response)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Could not get request for request lookup id {:?}.",
|
||||||
|
wait_payment_response.payment_identifier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks a specific mint quote as paid
|
||||||
|
///
|
||||||
|
/// Updates the mint quote with payment information and broadcasts
|
||||||
|
/// a notification about the payment status change.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `mint_quote` - The mint quote to mark as paid
|
||||||
|
/// * `wait_payment_response` - Payment response containing payment details
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` if the update was successful
|
||||||
|
/// * `Error` if the update fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn pay_mint_quote(
|
||||||
|
&self,
|
||||||
|
tx: &mut Box<dyn database::MintTransaction<'_, database::Error> + Send + Sync + '_>,
|
||||||
|
mint_quote: &MintQuote,
|
||||||
|
wait_payment_response: WaitPaymentResponse,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
tracing::debug!(
|
||||||
|
"Received payment notification of {} for mint quote {} with payment id {}",
|
||||||
|
wait_payment_response.payment_amount,
|
||||||
|
mint_quote.id,
|
||||||
|
wait_payment_response.payment_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let quote_state = mint_quote.state();
|
||||||
|
if !mint_quote
|
||||||
|
.payment_ids()
|
||||||
|
.contains(&&wait_payment_response.payment_id)
|
||||||
|
{
|
||||||
|
if mint_quote.payment_method == PaymentMethod::Bolt11
|
||||||
|
&& (quote_state == MintQuoteState::Issued || quote_state == MintQuoteState::Paid)
|
||||||
|
{
|
||||||
|
tracing::info!("Received payment notification for already seen payment.");
|
||||||
|
} else {
|
||||||
|
tx.increment_mint_quote_amount_paid(
|
||||||
|
&mint_quote.id,
|
||||||
|
wait_payment_response.payment_amount,
|
||||||
|
wait_payment_response.payment_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.pubsub_manager
|
||||||
|
.mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::info!("Received payment notification for already seen payment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the status of a mint quote and updates it if necessary
|
||||||
|
///
|
||||||
|
/// If the quote is unpaid, this will check if payment has been received.
|
||||||
|
/// Returns the current state of the quote.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `quote_id` - The UUID of the quote to check
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `MintQuoteResponse` - The current state of the quote
|
||||||
|
/// * `Error` if the quote doesn't exist or checking fails
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn check_mint_quote(&self, quote_id: &Uuid) -> Result<MintQuoteResponse, Error> {
|
||||||
|
let mut quote = self
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(quote_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnknownQuote)?;
|
||||||
|
|
||||||
|
self.check_mint_quote_paid(&mut quote).await?;
|
||||||
|
|
||||||
|
quote.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes a mint request to issue new tokens
|
||||||
|
///
|
||||||
|
/// This function:
|
||||||
|
/// 1. Verifies the mint quote exists and is paid
|
||||||
|
/// 2. Validates the request signature if a pubkey was provided
|
||||||
|
/// 3. Verifies the outputs match the expected amount
|
||||||
|
/// 4. Signs the blinded messages
|
||||||
|
/// 5. Updates the quote status
|
||||||
|
/// 6. Broadcasts a notification about the status change
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `mint_request` - The mint request containing blinded outputs to sign
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `MintBolt11Response` - Response containing blind signatures
|
||||||
|
/// * `Error` if validation fails or signing fails
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn process_mint_request(
|
||||||
|
&self,
|
||||||
|
mint_request: MintRequest<Uuid>,
|
||||||
|
) -> Result<MintResponse, Error> {
|
||||||
|
let mut mint_quote = self
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(&mint_request.quote)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnknownQuote)?;
|
||||||
|
|
||||||
|
self.check_mint_quote_paid(&mut mint_quote).await?;
|
||||||
|
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
|
||||||
|
let mint_quote = tx
|
||||||
|
.get_mint_quote(&mint_request.quote)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnknownQuote)?;
|
||||||
|
|
||||||
|
match mint_quote.state() {
|
||||||
|
MintQuoteState::Unpaid => {
|
||||||
|
return Err(Error::UnpaidQuote);
|
||||||
|
}
|
||||||
|
MintQuoteState::Issued => {
|
||||||
|
if mint_quote.payment_method == PaymentMethod::Bolt12
|
||||||
|
&& mint_quote.amount_paid() > mint_quote.amount_issued()
|
||||||
|
{
|
||||||
|
tracing::warn!("Mint quote should state should have been set to issued upon new payment. Something isn't right. Stopping mint");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::IssuedQuote);
|
||||||
|
}
|
||||||
|
MintQuoteState::Paid => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if mint_quote.payment_method == PaymentMethod::Bolt12 && mint_quote.pubkey.is_none() {
|
||||||
|
tracing::warn!("Bolt12 mint quote created without pubkey");
|
||||||
|
return Err(Error::SignatureMissingOrInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mint_amount = match mint_quote.payment_method {
|
||||||
|
PaymentMethod::Bolt11 => mint_quote.amount.ok_or(Error::AmountUndefined)?,
|
||||||
|
PaymentMethod::Bolt12 => {
|
||||||
|
if mint_quote.amount_issued() > mint_quote.amount_paid() {
|
||||||
|
tracing::error!(
|
||||||
|
"Quote state should not be issued if issued {} is > paid {}.",
|
||||||
|
mint_quote.amount_issued(),
|
||||||
|
mint_quote.amount_paid()
|
||||||
|
);
|
||||||
|
return Err(Error::UnpaidQuote);
|
||||||
|
}
|
||||||
|
mint_quote.amount_paid() - mint_quote.amount_issued()
|
||||||
|
}
|
||||||
|
_ => return Err(Error::UnsupportedPaymentMethod),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the there is a public key provoided in mint quote request
|
||||||
|
// verify the signature is provided for the mint request
|
||||||
|
if let Some(pubkey) = mint_quote.pubkey {
|
||||||
|
mint_request.verify_signature(pubkey)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Verification { amount, unit } =
|
||||||
|
match self.verify_outputs(&mut tx, &mint_request.outputs).await {
|
||||||
|
Ok(verification) => verification,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::debug!("Could not verify mint outputs");
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We check the total value of blinded messages == mint quote
|
||||||
|
if amount != mint_amount {
|
||||||
|
return Err(Error::TransactionUnbalanced(
|
||||||
|
mint_amount.into(),
|
||||||
|
mint_request.total_amount()?.into(),
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let unit = unit.ok_or(Error::UnsupportedUnit).unwrap();
|
||||||
|
ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
|
||||||
|
|
||||||
|
let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
|
||||||
|
|
||||||
|
for blinded_message in mint_request.outputs.iter() {
|
||||||
|
let blind_signature = self.blind_sign(blinded_message.clone()).await?;
|
||||||
|
blind_signatures.push(blind_signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.add_blind_signatures(
|
||||||
|
&mint_request
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.blinded_secret)
|
||||||
|
.collect::<Vec<PublicKey>>(),
|
||||||
|
&blind_signatures,
|
||||||
|
Some(mint_request.quote),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.increment_mint_quote_amount_issued(&mint_request.quote, mint_request.total_amount()?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
self.pubsub_manager
|
||||||
|
.mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
|
||||||
|
|
||||||
|
Ok(MintResponse {
|
||||||
|
signatures: blind_signatures,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
|
use cdk_common::amount::to_unit;
|
||||||
use cdk_common::common::PaymentProcessorKey;
|
use cdk_common::common::PaymentProcessorKey;
|
||||||
use cdk_common::database::{self, MintTransaction};
|
|
||||||
use cdk_common::mint::MintQuote;
|
use cdk_common::mint::MintQuote;
|
||||||
use cdk_common::MintQuoteState;
|
use cdk_common::util::unix_time;
|
||||||
|
use cdk_common::{MintQuoteState, PaymentMethod};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::Mint;
|
use super::Mint;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
impl Mint {
|
impl Mint {
|
||||||
/// Check the status of an ln payment for a quote
|
/// Check the status of an ln payment for a quote
|
||||||
pub async fn check_mint_quote_paid(
|
#[instrument(skip_all)]
|
||||||
&self,
|
pub async fn check_mint_quote_paid(&self, quote: &mut MintQuote) -> Result<(), Error> {
|
||||||
tx: Box<dyn MintTransaction<'_, database::Error> + Send + Sync + '_>,
|
let state = quote.state();
|
||||||
quote: &mut MintQuote,
|
|
||||||
) -> Result<Box<dyn MintTransaction<'_, database::Error> + Send + Sync + '_>, Error> {
|
// We can just return here and do not need to check with ln node.
|
||||||
|
// If quote is issued it is already in a final state,
|
||||||
|
// If it is paid ln node will only tell us what we already know
|
||||||
|
if quote.payment_method == PaymentMethod::Bolt11
|
||||||
|
&& (state == MintQuoteState::Issued || state == MintQuoteState::Paid)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let ln = match self.ln.get(&PaymentProcessorKey::new(
|
let ln = match self.ln.get(&PaymentProcessorKey::new(
|
||||||
quote.unit.clone(),
|
quote.unit.clone(),
|
||||||
cdk_common::PaymentMethod::Bolt11,
|
quote.payment_method.clone(),
|
||||||
)) {
|
)) {
|
||||||
Some(ln) => ln,
|
Some(ln) => ln,
|
||||||
None => {
|
None => {
|
||||||
@@ -25,23 +35,30 @@ impl Mint {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
let ln_status = ln
|
let ln_status = ln
|
||||||
.check_incoming_payment_status("e.request_lookup_id)
|
.check_incoming_payment_status("e.request_lookup_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
|
||||||
if ln_status != quote.state && quote.state != MintQuoteState::Issued {
|
for payment in ln_status {
|
||||||
tx.update_mint_quote_state("e.id, ln_status).await?;
|
if !quote.payment_ids().contains(&&payment.payment_id) {
|
||||||
|
tracing::debug!("Found payment for quote {} when checking.", quote.id);
|
||||||
|
let amount_paid = to_unit(payment.payment_amount, &payment.unit, "e.unit)?;
|
||||||
|
|
||||||
quote.state = ln_status;
|
quote.increment_amount_paid(amount_paid)?;
|
||||||
|
quote.add_payment(amount_paid, payment.payment_id.clone(), unix_time())?;
|
||||||
|
|
||||||
|
tx.increment_mint_quote_amount_paid("e.id, amount_paid, payment.payment_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
self.pubsub_manager
|
self.pubsub_manager
|
||||||
.mint_quote_bolt11_status(quote.clone(), ln_status);
|
.mint_quote_bolt11_status(quote.clone(), MintQuoteState::Paid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(tx)
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
|
use cdk_common::amount::amount_for_offer;
|
||||||
use cdk_common::database::{self, MintTransaction};
|
use cdk_common::database::{self, MintTransaction};
|
||||||
|
use cdk_common::melt::MeltQuoteRequest;
|
||||||
|
use cdk_common::mint::MeltPaymentRequest;
|
||||||
use cdk_common::nut00::ProofsMethods;
|
use cdk_common::nut00::ProofsMethods;
|
||||||
use cdk_common::nut05::MeltMethodOptions;
|
use cdk_common::nut05::MeltMethodOptions;
|
||||||
use cdk_common::MeltOptions;
|
use cdk_common::payment::{
|
||||||
use lightning_invoice::Bolt11Invoice;
|
Bolt11OutgoingPaymentOptions, Bolt12OutgoingPaymentOptions, OutgoingPaymentOptions,
|
||||||
|
PaymentQuoteOptions,
|
||||||
|
};
|
||||||
|
use cdk_common::{MeltOptions, MeltQuoteBolt12Request};
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -65,10 +72,12 @@ impl Mint {
|
|||||||
amount
|
amount
|
||||||
}
|
}
|
||||||
Some(MeltOptions::Amountless { amountless: _ }) => {
|
Some(MeltOptions::Amountless { amountless: _ }) => {
|
||||||
if !matches!(
|
if method == PaymentMethod::Bolt11
|
||||||
|
&& !matches!(
|
||||||
settings.options,
|
settings.options,
|
||||||
Some(MeltMethodOptions::Bolt11 { amountless: true })
|
Some(MeltMethodOptions::Bolt11 { amountless: true })
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return Err(Error::AmountlessInvoiceNotSupported(unit, method));
|
return Err(Error::AmountlessInvoiceNotSupported(unit, method));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +106,28 @@ impl Mint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get melt bolt11 quote
|
/// Get melt quote for either BOLT11 or BOLT12
|
||||||
|
///
|
||||||
|
/// This function accepts a `MeltQuoteRequest` enum and delegates to the
|
||||||
|
/// appropriate handler based on the request type.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn get_melt_bolt11_quote(
|
pub async fn get_melt_quote(
|
||||||
|
&self,
|
||||||
|
melt_quote_request: MeltQuoteRequest,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
||||||
|
match melt_quote_request {
|
||||||
|
MeltQuoteRequest::Bolt11(bolt11_request) => {
|
||||||
|
self.get_melt_bolt11_quote_impl(&bolt11_request).await
|
||||||
|
}
|
||||||
|
MeltQuoteRequest::Bolt12(bolt12_request) => {
|
||||||
|
self.get_melt_bolt12_quote_impl(&bolt12_request).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of get_melt_bolt11_quote
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn get_melt_bolt11_quote_impl(
|
||||||
&self,
|
&self,
|
||||||
melt_request: &MeltQuoteBolt11Request,
|
melt_request: &MeltQuoteBolt11Request,
|
||||||
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
||||||
@@ -110,6 +138,19 @@ impl Mint {
|
|||||||
..
|
..
|
||||||
} = melt_request;
|
} = melt_request;
|
||||||
|
|
||||||
|
let amount_msats = melt_request.amount_msat()?;
|
||||||
|
|
||||||
|
let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
|
||||||
|
|
||||||
|
self.check_melt_request_acceptable(
|
||||||
|
amount_quote_unit,
|
||||||
|
unit.clone(),
|
||||||
|
PaymentMethod::Bolt11,
|
||||||
|
request.to_string(),
|
||||||
|
*options,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let ln = self
|
let ln = self
|
||||||
.ln
|
.ln
|
||||||
.get(&PaymentProcessorKey::new(
|
.get(&PaymentProcessorKey::new(
|
||||||
@@ -122,11 +163,17 @@ impl Mint {
|
|||||||
Error::UnsupportedUnit
|
Error::UnsupportedUnit
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let bolt11 = Bolt11OutgoingPaymentOptions {
|
||||||
|
bolt11: melt_request.request.clone(),
|
||||||
|
max_fee_amount: None,
|
||||||
|
timeout_secs: None,
|
||||||
|
melt_options: melt_request.options,
|
||||||
|
};
|
||||||
|
|
||||||
let payment_quote = ln
|
let payment_quote = ln
|
||||||
.get_payment_quote(
|
.get_payment_quote(
|
||||||
&melt_request.request.to_string(),
|
|
||||||
&melt_request.unit,
|
&melt_request.unit,
|
||||||
melt_request.options,
|
OutgoingPaymentOptions::Bolt11(Box::new(bolt11)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
@@ -139,62 +186,137 @@ impl Mint {
|
|||||||
Error::UnsupportedUnit
|
Error::UnsupportedUnit
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.check_melt_request_acceptable(
|
|
||||||
payment_quote.amount,
|
|
||||||
unit.clone(),
|
|
||||||
PaymentMethod::Bolt11,
|
|
||||||
request.to_string(),
|
|
||||||
*options,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
let msats_to_pay = options.map(|opt| opt.amount_msat());
|
|
||||||
|
|
||||||
let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl;
|
let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl;
|
||||||
|
|
||||||
let quote = MeltQuote::new(
|
let quote = MeltQuote::new(
|
||||||
request.to_string(),
|
MeltPaymentRequest::Bolt11 {
|
||||||
|
bolt11: request.clone(),
|
||||||
|
},
|
||||||
unit.clone(),
|
unit.clone(),
|
||||||
payment_quote.amount,
|
payment_quote.amount,
|
||||||
payment_quote.fee,
|
payment_quote.fee,
|
||||||
unix_time() + melt_ttl,
|
unix_time() + melt_ttl,
|
||||||
payment_quote.request_lookup_id.clone(),
|
payment_quote.request_lookup_id.clone(),
|
||||||
msats_to_pay,
|
*options,
|
||||||
|
PaymentMethod::Bolt11,
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"New melt quote {} for {} {} with request id {}",
|
"New melt quote {} for {} {} with request id {}",
|
||||||
quote.id,
|
quote.id,
|
||||||
payment_quote.amount,
|
amount_quote_unit,
|
||||||
unit,
|
unit,
|
||||||
payment_quote.request_lookup_id
|
payment_quote.request_lookup_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut tx = self.localstore.begin_transaction().await?;
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
if let Some(mut from_db_quote) = tx.get_melt_quote("e.id).await? {
|
tx.add_melt_quote(quote.clone()).await?;
|
||||||
if from_db_quote.state != quote.state {
|
tx.commit().await?;
|
||||||
tx.update_melt_quote_state("e.id, from_db_quote.state)
|
|
||||||
|
Ok(quote.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of get_melt_bolt12_quote
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
async fn get_melt_bolt12_quote_impl(
|
||||||
|
&self,
|
||||||
|
melt_request: &MeltQuoteBolt12Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
||||||
|
let MeltQuoteBolt12Request {
|
||||||
|
request,
|
||||||
|
unit,
|
||||||
|
options,
|
||||||
|
} = melt_request;
|
||||||
|
|
||||||
|
let offer = Offer::from_str(request).map_err(|_| Error::InvalidPaymentRequest)?;
|
||||||
|
|
||||||
|
let amount = match options {
|
||||||
|
Some(options) => match options {
|
||||||
|
MeltOptions::Amountless { amountless } => {
|
||||||
|
to_unit(amountless.amount_msat, &CurrencyUnit::Msat, unit)?
|
||||||
|
}
|
||||||
|
_ => return Err(Error::UnsupportedUnit),
|
||||||
|
},
|
||||||
|
None => amount_for_offer(&offer, unit).map_err(|_| Error::UnsupportedUnit)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.check_melt_request_acceptable(
|
||||||
|
amount,
|
||||||
|
unit.clone(),
|
||||||
|
PaymentMethod::Bolt12,
|
||||||
|
request.clone(),
|
||||||
|
*options,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
from_db_quote.state = quote.state;
|
|
||||||
}
|
let ln = self
|
||||||
if from_db_quote.request_lookup_id != quote.request_lookup_id {
|
.ln
|
||||||
tx.update_melt_quote_request_lookup_id("e.id, "e.request_lookup_id)
|
.get(&PaymentProcessorKey::new(
|
||||||
.await?;
|
unit.clone(),
|
||||||
from_db_quote.request_lookup_id = quote.request_lookup_id.clone();
|
PaymentMethod::Bolt12,
|
||||||
}
|
))
|
||||||
if from_db_quote != quote {
|
.ok_or_else(|| {
|
||||||
return Err(Error::Internal);
|
tracing::info!("Could not get ln backend for {}, bolt12 ", unit);
|
||||||
}
|
|
||||||
} else if let Err(err) = tx.add_melt_quote(quote.clone()).await {
|
Error::UnsupportedUnit
|
||||||
match err {
|
})?;
|
||||||
database::Error::Duplicate => {
|
|
||||||
return Err(Error::RequestAlreadyPaid);
|
let offer = Offer::from_str(&melt_request.request).map_err(|_| Error::Bolt12parse)?;
|
||||||
}
|
|
||||||
_ => return Err(Error::from(err)),
|
let outgoing_payment_options = Bolt12OutgoingPaymentOptions {
|
||||||
}
|
offer: offer.clone(),
|
||||||
}
|
max_fee_amount: None,
|
||||||
|
timeout_secs: None,
|
||||||
|
melt_options: *options,
|
||||||
|
invoice: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let payment_quote = ln
|
||||||
|
.get_payment_quote(
|
||||||
|
&melt_request.unit,
|
||||||
|
OutgoingPaymentOptions::Bolt12(Box::new(outgoing_payment_options)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!(
|
||||||
|
"Could not get payment quote for mint quote, {} bolt12, {}",
|
||||||
|
unit,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
Error::UnsupportedUnit
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let invoice = payment_quote.options.and_then(|options| match options {
|
||||||
|
PaymentQuoteOptions::Bolt12 { invoice } => invoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
let payment_request = MeltPaymentRequest::Bolt12 {
|
||||||
|
offer: Box::new(offer),
|
||||||
|
invoice,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote = MeltQuote::new(
|
||||||
|
payment_request,
|
||||||
|
unit.clone(),
|
||||||
|
payment_quote.amount,
|
||||||
|
payment_quote.fee,
|
||||||
|
unix_time() + self.quote_ttl().await?.melt_ttl,
|
||||||
|
payment_quote.request_lookup_id.clone(),
|
||||||
|
*options,
|
||||||
|
PaymentMethod::Bolt12,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"New melt quote {} for {} {} with request id {}",
|
||||||
|
quote.id,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
payment_quote.request_lookup_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tx = self.localstore.begin_transaction().await?;
|
||||||
|
tx.add_melt_quote(quote.clone()).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(quote.into())
|
Ok(quote.into())
|
||||||
@@ -228,7 +350,7 @@ impl Mint {
|
|||||||
fee_reserve: quote.fee_reserve,
|
fee_reserve: quote.fee_reserve,
|
||||||
payment_preimage: quote.payment_preimage,
|
payment_preimage: quote.payment_preimage,
|
||||||
change,
|
change,
|
||||||
request: Some(quote.request.clone()),
|
request: Some(quote.request.to_string()),
|
||||||
unit: Some(quote.unit.clone()),
|
unit: Some(quote.unit.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -247,16 +369,40 @@ impl Mint {
|
|||||||
melt_quote: &MeltQuote,
|
melt_quote: &MeltQuote,
|
||||||
melt_request: &MeltRequest<Uuid>,
|
melt_request: &MeltRequest<Uuid>,
|
||||||
) -> Result<Option<Amount>, Error> {
|
) -> Result<Option<Amount>, Error> {
|
||||||
let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
|
|
||||||
|
|
||||||
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
|
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
|
||||||
.expect("Quote unit is checked above that it can convert to msat");
|
.expect("Quote unit is checked above that it can convert to msat");
|
||||||
|
|
||||||
let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
|
let invoice_amount_msats = match &melt_quote.request {
|
||||||
Some(amt) => amt.into(),
|
MeltPaymentRequest::Bolt11 { bolt11 } => match bolt11.amount_milli_satoshis() {
|
||||||
|
Some(amount) => amount.into(),
|
||||||
None => melt_quote
|
None => melt_quote
|
||||||
.msat_to_pay
|
.options
|
||||||
.ok_or(Error::InvoiceAmountUndefined)?,
|
.ok_or(Error::InvoiceAmountUndefined)?
|
||||||
|
.amount_msat(),
|
||||||
|
},
|
||||||
|
MeltPaymentRequest::Bolt12 { offer, invoice: _ } => match offer.amount() {
|
||||||
|
Some(amount) => {
|
||||||
|
let (amount, currency) = match amount {
|
||||||
|
lightning::offers::offer::Amount::Bitcoin { amount_msats } => {
|
||||||
|
(amount_msats, CurrencyUnit::Msat)
|
||||||
|
}
|
||||||
|
lightning::offers::offer::Amount::Currency {
|
||||||
|
iso4217_code,
|
||||||
|
amount,
|
||||||
|
} => (
|
||||||
|
amount,
|
||||||
|
CurrencyUnit::from_str(&String::from_utf8(iso4217_code.to_vec())?)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
to_unit(amount, ¤cy, &CurrencyUnit::Msat)
|
||||||
|
.map_err(|_err| Error::UnsupportedUnit)?
|
||||||
|
}
|
||||||
|
None => melt_quote
|
||||||
|
.options
|
||||||
|
.ok_or(Error::InvoiceAmountUndefined)?
|
||||||
|
.amount_msat(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let partial_amount = match invoice_amount_msats > quote_msats {
|
let partial_amount = match invoice_amount_msats > quote_msats {
|
||||||
@@ -273,7 +419,7 @@ impl Mint {
|
|||||||
.map_err(|_| Error::UnsupportedUnit)?,
|
.map_err(|_| Error::UnsupportedUnit)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
|
let inputs_amount_quote_unit = melt_request.inputs_amount().map_err(|_| {
|
||||||
tracing::error!("Proof inputs in melt quote overflowed");
|
tracing::error!("Proof inputs in melt quote overflowed");
|
||||||
Error::AmountOverflow
|
Error::AmountOverflow
|
||||||
})?;
|
})?;
|
||||||
@@ -305,7 +451,7 @@ impl Mint {
|
|||||||
melt_request: &MeltRequest<Uuid>,
|
melt_request: &MeltRequest<Uuid>,
|
||||||
) -> Result<(ProofWriter, MeltQuote), Error> {
|
) -> Result<(ProofWriter, MeltQuote), Error> {
|
||||||
let (state, quote) = tx
|
let (state, quote) = tx
|
||||||
.update_melt_quote_state(melt_request.quote(), MeltQuoteState::Pending)
|
.update_melt_quote_state(melt_request.quote(), MeltQuoteState::Pending, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
@@ -371,7 +517,7 @@ impl Mint {
|
|||||||
|
|
||||||
/// Melt Bolt11
|
/// Melt Bolt11
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn melt_bolt11(
|
pub async fn melt(
|
||||||
&self,
|
&self,
|
||||||
melt_request: &MeltRequest<Uuid>,
|
melt_request: &MeltRequest<Uuid>,
|
||||||
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
|
||||||
@@ -455,7 +601,7 @@ impl Mint {
|
|||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
let pre = match ln
|
let pre = match ln
|
||||||
.make_payment(quote.clone(), partial_amount, Some(quote.fee_reserve))
|
.make_payment("e.unit, quote.clone().try_into()?)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(pay)
|
Ok(pay)
|
||||||
@@ -602,7 +748,11 @@ impl Mint {
|
|||||||
.update_proofs_states(&mut tx, &input_ys, State::Spent)
|
.update_proofs_states(&mut tx, &input_ys, State::Spent)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.update_melt_quote_state(melt_request.quote(), MeltQuoteState::Paid)
|
tx.update_melt_quote_state(
|
||||||
|
melt_request.quote(),
|
||||||
|
MeltQuoteState::Paid,
|
||||||
|
payment_preimage.clone(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.pubsub_manager.melt_quote_status(
|
self.pubsub_manager.melt_quote_status(
|
||||||
@@ -615,7 +765,7 @@ impl Mint {
|
|||||||
let mut change = None;
|
let mut change = None;
|
||||||
|
|
||||||
// Check if there is change to return
|
// Check if there is change to return
|
||||||
if melt_request.proofs_amount()? > total_spent {
|
if melt_request.inputs_amount()? > total_spent {
|
||||||
// Check if wallet provided change outputs
|
// Check if wallet provided change outputs
|
||||||
if let Some(outputs) = melt_request.outputs().clone() {
|
if let Some(outputs) = melt_request.outputs().clone() {
|
||||||
let blinded_messages: Vec<PublicKey> =
|
let blinded_messages: Vec<PublicKey> =
|
||||||
@@ -636,7 +786,7 @@ impl Mint {
|
|||||||
|
|
||||||
let fee = self.get_proofs_fee(melt_request.inputs()).await?;
|
let fee = self.get_proofs_fee(melt_request.inputs()).await?;
|
||||||
|
|
||||||
let change_target = melt_request.proofs_amount()? - total_spent - fee;
|
let change_target = melt_request.inputs_amount()? - total_spent - fee;
|
||||||
|
|
||||||
let mut amounts = change_target.split();
|
let mut amounts = change_target.split();
|
||||||
let mut change_sigs = Vec::with_capacity(amounts.len());
|
let mut change_sigs = Vec::with_capacity(amounts.len());
|
||||||
@@ -689,7 +839,7 @@ impl Mint {
|
|||||||
fee_reserve: quote.fee_reserve,
|
fee_reserve: quote.fee_reserve,
|
||||||
state: MeltQuoteState::Paid,
|
state: MeltQuoteState::Paid,
|
||||||
expiry: quote.expiry,
|
expiry: quote.expiry,
|
||||||
request: Some(quote.request.clone()),
|
request: Some(quote.request.to_string()),
|
||||||
unit: Some(quote.unit.clone()),
|
unit: Some(quote.unit.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,11 +254,32 @@ impl Mint {
|
|||||||
|
|
||||||
let mut join_set = JoinSet::new();
|
let mut join_set = JoinSet::new();
|
||||||
|
|
||||||
|
let mut processor_groups: Vec<(
|
||||||
|
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
||||||
|
Vec<PaymentProcessorKey>,
|
||||||
|
)> = Vec::new();
|
||||||
|
|
||||||
for (key, ln) in self.ln.iter() {
|
for (key, ln) in self.ln.iter() {
|
||||||
|
// Check if we already have this processor
|
||||||
|
let found = processor_groups.iter_mut().find(|(proc_ref, _)| {
|
||||||
|
// Compare Arc pointer equality using ptr_eq
|
||||||
|
Arc::ptr_eq(proc_ref, ln)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((_, keys)) = found {
|
||||||
|
// We found this processor, add the key to its group
|
||||||
|
keys.push(key.clone());
|
||||||
|
} else {
|
||||||
|
// New processor, create a new group
|
||||||
|
processor_groups.push((Arc::clone(ln), vec![key.clone()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ln, key) in processor_groups {
|
||||||
if !ln.is_wait_invoice_active() {
|
if !ln.is_wait_invoice_active() {
|
||||||
tracing::info!("Wait payment for {:?} inactive starting.", key);
|
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 {
|
||||||
@@ -274,7 +295,7 @@ impl Mint {
|
|||||||
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 {
|
||||||
if let Err(err) = mint.pay_mint_quote_for_request_id(&request_lookup_id).await {
|
if let Err(err) = mint.pay_mint_quote_for_request_id(request_lookup_id).await {
|
||||||
tracing::warn!("{:?}", err);
|
tracing::warn!("{:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,7 +437,10 @@ impl Mint {
|
|||||||
melt_quote: &MeltQuote,
|
melt_quote: &MeltQuote,
|
||||||
melt_request: &MeltRequest<Uuid>,
|
melt_request: &MeltRequest<Uuid>,
|
||||||
) -> Result<Option<Amount>, Error> {
|
) -> Result<Option<Amount>, Error> {
|
||||||
let mint_quote = match tx.get_mint_quote_by_request(&melt_quote.request).await {
|
let mint_quote = match tx
|
||||||
|
.get_mint_quote_by_request(&melt_quote.request.to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Some(mint_quote)) => mint_quote,
|
Ok(Some(mint_quote)) => mint_quote,
|
||||||
// Not an internal melt -> mint
|
// Not an internal melt -> mint
|
||||||
Ok(None) => return Ok(None),
|
Ok(None) => return Ok(None),
|
||||||
@@ -428,31 +452,32 @@ impl Mint {
|
|||||||
tracing::error!("internal stuff");
|
tracing::error!("internal stuff");
|
||||||
|
|
||||||
// Mint quote has already been settled, proofs should not be burned or held.
|
// Mint quote has already been settled, proofs should not be burned or held.
|
||||||
if mint_quote.state == MintQuoteState::Issued || mint_quote.state == MintQuoteState::Paid {
|
if mint_quote.state() == MintQuoteState::Issued
|
||||||
|
|| mint_quote.state() == MintQuoteState::Paid
|
||||||
|
{
|
||||||
return Err(Error::RequestAlreadyPaid);
|
return Err(Error::RequestAlreadyPaid);
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
|
let inputs_amount_quote_unit = melt_request.inputs_amount().map_err(|_| {
|
||||||
tracing::error!("Proof inputs in melt quote overflowed");
|
tracing::error!("Proof inputs in melt quote overflowed");
|
||||||
Error::AmountOverflow
|
Error::AmountOverflow
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut mint_quote = mint_quote;
|
if let Some(amount) = mint_quote.amount {
|
||||||
|
if amount > inputs_amount_quote_unit {
|
||||||
if mint_quote.amount > inputs_amount_quote_unit {
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Not enough inuts provided: {} needed {}",
|
"Not enough inuts provided: {} needed {}",
|
||||||
inputs_amount_quote_unit,
|
inputs_amount_quote_unit,
|
||||||
mint_quote.amount
|
amount
|
||||||
);
|
);
|
||||||
return Err(Error::InsufficientFunds);
|
return Err(Error::InsufficientFunds);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mint_quote.state = MintQuoteState::Paid;
|
|
||||||
|
|
||||||
let amount = melt_quote.amount;
|
let amount = melt_quote.amount;
|
||||||
|
|
||||||
tx.add_or_replace_mint_quote(mint_quote).await?;
|
tx.increment_mint_quote_amount_paid(&mint_quote.id, amount, melt_quote.id.to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(Some(amount))
|
Ok(Some(amount))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::types::PaymentProcessorKey;
|
|||||||
impl Mint {
|
impl Mint {
|
||||||
/// Checks the states of melt quotes that are **PENDING** or **UNKNOWN** to the mint with the ln node
|
/// Checks the states of melt quotes that are **PENDING** or **UNKNOWN** to the mint with the ln node
|
||||||
pub async fn check_pending_melt_quotes(&self) -> Result<(), Error> {
|
pub async fn check_pending_melt_quotes(&self) -> Result<(), Error> {
|
||||||
let melt_quotes = self.localstore.get_melt_quotes().await?;
|
let melt_quotes = self.localstore.get_melt_quotes().await.unwrap();
|
||||||
let pending_quotes: Vec<MeltQuote> = melt_quotes
|
let pending_quotes: Vec<MeltQuote> = melt_quotes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown)
|
.filter(|q| q.state == MeltQuoteState::Pending || q.state == MeltQuoteState::Unknown)
|
||||||
@@ -53,7 +53,11 @@ impl Mint {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = tx
|
if let Err(err) = tx
|
||||||
.update_melt_quote_state(&pending_quote.id, melt_quote_state)
|
.update_melt_quote_state(
|
||||||
|
&pending_quote.id,
|
||||||
|
melt_quote_state,
|
||||||
|
pay_invoice_response.payment_proof,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ impl OnNewSubscription for OnSubscription {
|
|||||||
Notification::MintQuoteBolt11(uuid) => {
|
Notification::MintQuoteBolt11(uuid) => {
|
||||||
mint_queries.push(datastore.get_mint_quote(uuid))
|
mint_queries.push(datastore.get_mint_quote(uuid))
|
||||||
}
|
}
|
||||||
|
Notification::MintQuoteBolt12(uuid) => {
|
||||||
|
mint_queries.push(datastore.get_mint_quote(uuid))
|
||||||
|
}
|
||||||
|
Notification::MeltQuoteBolt12(uuid) => {
|
||||||
|
melt_queries.push(datastore.get_melt_quote(uuid))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use cdk_common::nut04::MintMethodOptions;
|
use cdk_common::nut04::MintMethodOptions;
|
||||||
use cdk_common::wallet::{Transaction, TransactionDirection};
|
use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
|
||||||
|
use cdk_common::PaymentMethod;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::MintQuote;
|
|
||||||
use crate::amount::SplitTarget;
|
use crate::amount::SplitTarget;
|
||||||
use crate::dhke::construct_proofs;
|
use crate::dhke::construct_proofs;
|
||||||
use crate::nuts::nut00::ProofsMethods;
|
use crate::nuts::nut00::ProofsMethods;
|
||||||
@@ -81,16 +81,16 @@ impl Wallet {
|
|||||||
|
|
||||||
let quote_res = self.client.post_mint_quote(request).await?;
|
let quote_res = self.client.post_mint_quote(request).await?;
|
||||||
|
|
||||||
let quote = MintQuote {
|
let quote = MintQuote::new(
|
||||||
|
quote_res.quote,
|
||||||
mint_url,
|
mint_url,
|
||||||
id: quote_res.quote,
|
PaymentMethod::Bolt11,
|
||||||
amount,
|
Some(amount),
|
||||||
unit,
|
unit,
|
||||||
request: quote_res.request,
|
quote_res.request,
|
||||||
state: quote_res.state,
|
quote_res.expiry.unwrap_or(0),
|
||||||
expiry: quote_res.expiry.unwrap_or(0),
|
Some(secret_key),
|
||||||
secret_key: Some(secret_key),
|
);
|
||||||
};
|
|
||||||
|
|
||||||
self.localstore.add_mint_quote(quote.clone()).await?;
|
self.localstore.add_mint_quote(quote.clone()).await?;
|
||||||
|
|
||||||
@@ -196,6 +196,17 @@ impl Wallet {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(Error::UnknownQuote)?;
|
.ok_or(Error::UnknownQuote)?;
|
||||||
|
|
||||||
|
if quote_info.payment_method != PaymentMethod::Bolt11 {
|
||||||
|
return Err(Error::UnsupportedPaymentMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
let amount_mintable = quote_info.amount_mintable();
|
||||||
|
|
||||||
|
if amount_mintable == Amount::ZERO {
|
||||||
|
tracing::debug!("Amount mintable 0.");
|
||||||
|
return Err(Error::AmountUndefined);
|
||||||
|
}
|
||||||
|
|
||||||
let unix_time = unix_time();
|
let unix_time = unix_time();
|
||||||
|
|
||||||
if quote_info.expiry > unix_time {
|
if quote_info.expiry > unix_time {
|
||||||
@@ -214,7 +225,7 @@ impl Wallet {
|
|||||||
let premint_secrets = match &spending_conditions {
|
let premint_secrets = match &spending_conditions {
|
||||||
Some(spending_conditions) => PreMintSecrets::with_conditions(
|
Some(spending_conditions) => PreMintSecrets::with_conditions(
|
||||||
active_keyset_id,
|
active_keyset_id,
|
||||||
quote_info.amount,
|
amount_mintable,
|
||||||
&amount_split_target,
|
&amount_split_target,
|
||||||
spending_conditions,
|
spending_conditions,
|
||||||
)?,
|
)?,
|
||||||
@@ -222,7 +233,7 @@ impl Wallet {
|
|||||||
active_keyset_id,
|
active_keyset_id,
|
||||||
count,
|
count,
|
||||||
self.xpriv,
|
self.xpriv,
|
||||||
quote_info.amount,
|
amount_mintable,
|
||||||
&amount_split_target,
|
&amount_split_target,
|
||||||
)?,
|
)?,
|
||||||
};
|
};
|
||||||
258
crates/cdk/src/wallet/issue/issue_bolt12.rs
Normal file
258
crates/cdk/src/wallet/issue/issue_bolt12.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use cdk_common::nut04::MintMethodOptions;
|
||||||
|
use cdk_common::nut24::MintQuoteBolt12Request;
|
||||||
|
use cdk_common::wallet::{Transaction, TransactionDirection};
|
||||||
|
use cdk_common::{Proofs, SecretKey};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::amount::SplitTarget;
|
||||||
|
use crate::dhke::construct_proofs;
|
||||||
|
use crate::nuts::nut00::ProofsMethods;
|
||||||
|
use crate::nuts::{
|
||||||
|
nut12, MintQuoteBolt12Response, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use crate::types::ProofInfo;
|
||||||
|
use crate::util::unix_time;
|
||||||
|
use crate::wallet::MintQuote;
|
||||||
|
use crate::{Amount, Error, Wallet};
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
/// Mint Bolt12
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn mint_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
amount: Option<Amount>,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<MintQuote, Error> {
|
||||||
|
let mint_url = self.mint_url.clone();
|
||||||
|
let unit = &self.unit;
|
||||||
|
|
||||||
|
// If we have a description, we check that the mint supports it.
|
||||||
|
if description.is_some() {
|
||||||
|
let mint_method_settings = self
|
||||||
|
.localstore
|
||||||
|
.get_mint(mint_url.clone())
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::IncorrectMint)?
|
||||||
|
.nuts
|
||||||
|
.nut04
|
||||||
|
.get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)
|
||||||
|
.ok_or(Error::UnsupportedUnit)?;
|
||||||
|
|
||||||
|
match mint_method_settings.options {
|
||||||
|
Some(MintMethodOptions::Bolt11 { description }) if description => (),
|
||||||
|
_ => return Err(Error::InvoiceDescriptionUnsupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret_key = SecretKey::generate();
|
||||||
|
|
||||||
|
let mint_request = MintQuoteBolt12Request {
|
||||||
|
amount,
|
||||||
|
unit: self.unit.clone(),
|
||||||
|
description,
|
||||||
|
pubkey: secret_key.public_key(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = self.client.post_mint_bolt12_quote(mint_request).await?;
|
||||||
|
|
||||||
|
let quote = MintQuote::new(
|
||||||
|
quote_res.quote,
|
||||||
|
mint_url,
|
||||||
|
PaymentMethod::Bolt12,
|
||||||
|
amount,
|
||||||
|
unit.clone(),
|
||||||
|
quote_res.request,
|
||||||
|
quote_res.expiry.unwrap_or(0),
|
||||||
|
Some(secret_key),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.localstore.add_mint_quote(quote.clone()).await?;
|
||||||
|
|
||||||
|
Ok(quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint bolt12
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn mint_bolt12(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
amount: Option<Amount>,
|
||||||
|
amount_split_target: SplitTarget,
|
||||||
|
spending_conditions: Option<SpendingConditions>,
|
||||||
|
) -> Result<Proofs, Error> {
|
||||||
|
// Check that mint is in store of mints
|
||||||
|
if self
|
||||||
|
.localstore
|
||||||
|
.get_mint(self.mint_url.clone())
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
self.get_mint_info().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote_info = self.localstore.get_mint_quote(quote_id).await?;
|
||||||
|
|
||||||
|
let quote_info = if let Some(quote) = quote_info {
|
||||||
|
if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
|
||||||
|
return Err(Error::ExpiredQuote(quote.expiry, unix_time()));
|
||||||
|
}
|
||||||
|
|
||||||
|
quote.clone()
|
||||||
|
} else {
|
||||||
|
return Err(Error::UnknownQuote);
|
||||||
|
};
|
||||||
|
|
||||||
|
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
|
let count = self
|
||||||
|
.localstore
|
||||||
|
.get_keyset_counter(&active_keyset_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let count = count.map_or(0, |c| c + 1);
|
||||||
|
|
||||||
|
let amount = match amount {
|
||||||
|
Some(amount) => amount,
|
||||||
|
None => {
|
||||||
|
// If an amount it not supplied with check the status of the quote
|
||||||
|
// The mint will tell us how much can be minted
|
||||||
|
let state = self.mint_bolt12_quote_state(quote_id).await?;
|
||||||
|
|
||||||
|
state.amount_paid - state.amount_issued
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if amount == Amount::ZERO {
|
||||||
|
tracing::error!("Cannot mint zero amount.");
|
||||||
|
return Err(Error::InvoiceAmountUndefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
let premint_secrets = match &spending_conditions {
|
||||||
|
Some(spending_conditions) => PreMintSecrets::with_conditions(
|
||||||
|
active_keyset_id,
|
||||||
|
amount,
|
||||||
|
&amount_split_target,
|
||||||
|
spending_conditions,
|
||||||
|
)?,
|
||||||
|
None => PreMintSecrets::from_xpriv(
|
||||||
|
active_keyset_id,
|
||||||
|
count,
|
||||||
|
self.xpriv,
|
||||||
|
amount,
|
||||||
|
&amount_split_target,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut request = MintRequest {
|
||||||
|
quote: quote_id.to_string(),
|
||||||
|
outputs: premint_secrets.blinded_messages(),
|
||||||
|
signature: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(secret_key) = quote_info.secret_key.clone() {
|
||||||
|
request.sign(secret_key)?;
|
||||||
|
} else {
|
||||||
|
tracing::error!("Signature is required for bolt12.");
|
||||||
|
return Err(Error::SignatureMissingOrInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mint_res = self.client.post_mint(request).await?;
|
||||||
|
|
||||||
|
let keys = self.get_keyset_keys(active_keyset_id).await?;
|
||||||
|
|
||||||
|
// Verify the signature DLEQ is valid
|
||||||
|
{
|
||||||
|
for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
|
||||||
|
let keys = self.get_keyset_keys(sig.keyset_id).await?;
|
||||||
|
let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
|
||||||
|
match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
|
||||||
|
Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
|
||||||
|
Err(_) => return Err(Error::CouldNotVerifyDleq),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let proofs = construct_proofs(
|
||||||
|
mint_res.signatures,
|
||||||
|
premint_secrets.rs(),
|
||||||
|
premint_secrets.secrets(),
|
||||||
|
&keys,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Remove filled quote from store
|
||||||
|
let mut quote_info = self
|
||||||
|
.localstore
|
||||||
|
.get_mint_quote(quote_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnpaidQuote)?;
|
||||||
|
quote_info.amount_issued += proofs.total_amount()?;
|
||||||
|
|
||||||
|
self.localstore.add_mint_quote(quote_info.clone()).await?;
|
||||||
|
|
||||||
|
if spending_conditions.is_none() {
|
||||||
|
// Update counter for keyset
|
||||||
|
self.localstore
|
||||||
|
.increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proof_infos = proofs
|
||||||
|
.iter()
|
||||||
|
.map(|proof| {
|
||||||
|
ProofInfo::new(
|
||||||
|
proof.clone(),
|
||||||
|
self.mint_url.clone(),
|
||||||
|
State::Unspent,
|
||||||
|
quote_info.unit.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<ProofInfo>, _>>()?;
|
||||||
|
|
||||||
|
// Add new proofs to store
|
||||||
|
self.localstore.update_proofs(proof_infos, vec![]).await?;
|
||||||
|
|
||||||
|
// Add transaction to store
|
||||||
|
self.localstore
|
||||||
|
.add_transaction(Transaction {
|
||||||
|
mint_url: self.mint_url.clone(),
|
||||||
|
direction: TransactionDirection::Incoming,
|
||||||
|
amount: proofs.total_amount()?,
|
||||||
|
fee: Amount::ZERO,
|
||||||
|
unit: self.unit.clone(),
|
||||||
|
ys: proofs.ys()?,
|
||||||
|
timestamp: unix_time(),
|
||||||
|
memo: None,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(proofs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check mint quote status
|
||||||
|
#[instrument(skip(self, quote_id))]
|
||||||
|
pub async fn mint_bolt12_quote_state(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error> {
|
||||||
|
let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
|
||||||
|
|
||||||
|
match self.localstore.get_mint_quote(quote_id).await? {
|
||||||
|
Some(quote) => {
|
||||||
|
let mut quote = quote;
|
||||||
|
quote.amount_issued = response.amount_issued;
|
||||||
|
quote.amount_paid = response.amount_paid;
|
||||||
|
|
||||||
|
self.localstore.add_mint_quote(quote).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::info!("Quote mint {} unknown", quote_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/cdk/src/wallet/issue/mod.rs
Normal file
2
crates/cdk/src/wallet/issue/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod issue_bolt11;
|
||||||
|
mod issue_bolt12;
|
||||||
@@ -6,7 +6,6 @@ use cdk_common::wallet::{Transaction, TransactionDirection};
|
|||||||
use lightning_invoice::Bolt11Invoice;
|
use lightning_invoice::Bolt11Invoice;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::MeltQuote;
|
|
||||||
use crate::amount::to_unit;
|
use crate::amount::to_unit;
|
||||||
use crate::dhke::construct_proofs;
|
use crate::dhke::construct_proofs;
|
||||||
use crate::nuts::{
|
use crate::nuts::{
|
||||||
@@ -15,6 +14,7 @@ use crate::nuts::{
|
|||||||
};
|
};
|
||||||
use crate::types::{Melted, ProofInfo};
|
use crate::types::{Melted, ProofInfo};
|
||||||
use crate::util::unix_time;
|
use crate::util::unix_time;
|
||||||
|
use crate::wallet::MeltQuote;
|
||||||
use crate::{ensure_cdk, Error, Wallet};
|
use crate::{ensure_cdk, Error, Wallet};
|
||||||
|
|
||||||
impl Wallet {
|
impl Wallet {
|
||||||
89
crates/cdk/src/wallet/melt/melt_bolt12.rs
Normal file
89
crates/cdk/src/wallet/melt/melt_bolt12.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//! Melt BOLT12
|
||||||
|
//!
|
||||||
|
//! Implementation of melt functionality for BOLT12 offers
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use cdk_common::amount::amount_for_offer;
|
||||||
|
use cdk_common::wallet::MeltQuote;
|
||||||
|
use lightning::offers::offer::Offer;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::amount::to_unit;
|
||||||
|
use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
|
||||||
|
use crate::{Error, Wallet};
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
/// Melt Quote for BOLT12 offer
|
||||||
|
#[instrument(skip(self, request))]
|
||||||
|
pub async fn melt_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: String,
|
||||||
|
options: Option<MeltOptions>,
|
||||||
|
) -> Result<MeltQuote, Error> {
|
||||||
|
let quote_request = MeltQuoteBolt12Request {
|
||||||
|
request: request.clone(),
|
||||||
|
unit: self.unit.clone(),
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = self.client.post_melt_bolt12_quote(quote_request).await?;
|
||||||
|
|
||||||
|
if self.unit == CurrencyUnit::Sat || self.unit == CurrencyUnit::Msat {
|
||||||
|
let offer = Offer::from_str(&request).map_err(|_| Error::Bolt12parse)?;
|
||||||
|
// Get amount from offer or options
|
||||||
|
let amount_msat = options
|
||||||
|
.map(|opt| opt.amount_msat())
|
||||||
|
.or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
|
||||||
|
.ok_or(Error::AmountUndefined)?;
|
||||||
|
let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit).unwrap();
|
||||||
|
|
||||||
|
if quote_res.amount != amount_quote_unit {
|
||||||
|
tracing::warn!(
|
||||||
|
"Mint returned incorrect quote amount. Expected {}, got {}",
|
||||||
|
amount_quote_unit,
|
||||||
|
quote_res.amount
|
||||||
|
);
|
||||||
|
return Err(Error::IncorrectQuoteAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote = MeltQuote {
|
||||||
|
id: quote_res.quote,
|
||||||
|
amount: quote_res.amount,
|
||||||
|
request,
|
||||||
|
unit: self.unit.clone(),
|
||||||
|
fee_reserve: quote_res.fee_reserve,
|
||||||
|
state: quote_res.state,
|
||||||
|
expiry: quote_res.expiry,
|
||||||
|
payment_preimage: quote_res.payment_preimage,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.localstore.add_melt_quote(quote.clone()).await?;
|
||||||
|
|
||||||
|
Ok(quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BOLT12 melt quote status
|
||||||
|
#[instrument(skip(self, quote_id))]
|
||||||
|
pub async fn melt_bolt12_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let response = self.client.get_melt_bolt12_quote_status(quote_id).await?;
|
||||||
|
|
||||||
|
match self.localstore.get_melt_quote(quote_id).await? {
|
||||||
|
Some(quote) => {
|
||||||
|
let mut quote = quote;
|
||||||
|
|
||||||
|
quote.state = response.state;
|
||||||
|
self.localstore.add_melt_quote(quote).await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::info!("Quote melt {} unknown", quote_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/cdk/src/wallet/melt/mod.rs
Normal file
2
crates/cdk/src/wallet/melt/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod melt_bolt11;
|
||||||
|
mod melt_bolt12;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
||||||
use reqwest::{Client, IntoUrl};
|
use reqwest::{Client, IntoUrl};
|
||||||
@@ -91,7 +92,9 @@ impl HttpClientCore {
|
|||||||
let response = request
|
let response = request
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?
|
.map_err(|e| Error::HttpError(e.to_string()))?;
|
||||||
|
|
||||||
|
let response = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?;
|
.map_err(|e| Error::HttpError(e.to_string()))?;
|
||||||
@@ -395,6 +398,103 @@ impl MintConnector for HttpClient {
|
|||||||
let auth_token = None;
|
let auth_token = None;
|
||||||
self.core.http_post(url, auth_token, &request).await
|
self.core.http_post(url, auth_token, &request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mint Quote Bolt12 [NUT-23]
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_mint_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MintQuoteBolt12Request,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "mint", "quote", "bolt12"])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MintQuoteBolt12)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Quote Bolt12 status
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_mint_quote_bolt12_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "mint", "quote", "bolt12", quote_id])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Get, RoutePath::MintQuoteBolt12)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_get(url, auth_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Quote Bolt12 [NUT-23]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_melt_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MeltQuoteBolt12Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "melt", "quote", "bolt12"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MeltQuoteBolt12)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Quote Bolt12 Status [NUT-23]
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_melt_bolt12_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "melt", "quote", "bolt12", quote_id])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Get, RoutePath::MeltQuoteBolt12)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_get(url, auth_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Bolt12 [NUT-23]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_melt_bolt12(
|
||||||
|
&self,
|
||||||
|
request: MeltRequest<String>,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "melt", "bolt12"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MeltBolt12)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Http Client
|
/// Http Client
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use cdk_common::{MeltQuoteBolt12Request, MintQuoteBolt12Request, MintQuoteBolt12Response};
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::nuts::{
|
use crate::nuts::{
|
||||||
@@ -77,4 +78,29 @@ pub trait MintConnector: Debug {
|
|||||||
/// Set auth wallet on client
|
/// Set auth wallet on client
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
async fn set_auth_wallet(&self, wallet: Option<AuthWallet>);
|
async fn set_auth_wallet(&self, wallet: Option<AuthWallet>);
|
||||||
|
/// Mint Quote [NUT-04]
|
||||||
|
async fn post_mint_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MintQuoteBolt12Request,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error>;
|
||||||
|
/// Mint Quote status
|
||||||
|
async fn get_mint_quote_bolt12_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt12Response<String>, Error>;
|
||||||
|
/// Melt Quote [NUT-23]
|
||||||
|
async fn post_melt_bolt12_quote(
|
||||||
|
&self,
|
||||||
|
request: MeltQuoteBolt12Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Melt Quote Status [NUT-23]
|
||||||
|
async fn get_melt_bolt12_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Melt [NUT-23]
|
||||||
|
async fn post_melt_bolt12(
|
||||||
|
&self,
|
||||||
|
request: MeltRequest<String>,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ use crate::OidcClient;
|
|||||||
mod auth;
|
mod auth;
|
||||||
mod balance;
|
mod balance;
|
||||||
mod builder;
|
mod builder;
|
||||||
|
mod issue;
|
||||||
mod keysets;
|
mod keysets;
|
||||||
mod melt;
|
mod melt;
|
||||||
mod mint;
|
|
||||||
mod mint_connector;
|
mod mint_connector;
|
||||||
pub mod multi_mint_wallet;
|
pub mod multi_mint_wallet;
|
||||||
mod proofs;
|
mod proofs;
|
||||||
|
|||||||
@@ -224,6 +224,13 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Running regtest test with cln mint for bolt12"
|
||||||
|
cargo test -p cdk-integration-tests --test bolt12
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "regtest test failed, exiting"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Switch Mints: Run tests with LND mint
|
# Switch Mints: Run tests with LND mint
|
||||||
echo "Switching to LND mint for tests"
|
echo "Switching to LND mint for tests"
|
||||||
export CDK_ITESTS_MINT_PORT_0=8087
|
export CDK_ITESTS_MINT_PORT_0=8087
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ cleanup() {
|
|||||||
unset CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS
|
unset CDK_MINTD_GRPC_PAYMENT_PROCESSOR_SUPPORTED_UNITS
|
||||||
unset CDK_MINTD_MNEMONIC
|
unset CDK_MINTD_MNEMONIC
|
||||||
unset CDK_MINTD_PID
|
unset CDK_MINTD_PID
|
||||||
|
unset CDK_PAYMENT_PROCESSOR_CLN_BOLT12
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set up trap to call cleanup on script exit
|
# Set up trap to call cleanup on script exit
|
||||||
@@ -102,6 +103,7 @@ if [ "$LN_BACKEND" != "FAKEWALLET" ]; then
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "Regtest set up continuing"
|
echo "Regtest set up continuing"
|
||||||
|
export CDK_PAYMENT_PROCESSOR_CLN_BOLT12=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start payment processor
|
# Start payment processor
|
||||||
@@ -177,5 +179,17 @@ cargo test -p cdk-integration-tests --test happy_path_mint_wallet
|
|||||||
# Capture the exit status of cargo test
|
# Capture the exit status of cargo test
|
||||||
test_status=$?
|
test_status=$?
|
||||||
|
|
||||||
|
if [ "$LN_BACKEND" = "CLN" ]; then
|
||||||
|
echo "Running bolt12 tests for CLN backend"
|
||||||
|
cargo test -p cdk-integration-tests --test bolt12
|
||||||
|
bolt12_test_status=$?
|
||||||
|
|
||||||
|
# Exit with non-zero status if either test failed
|
||||||
|
if [ $test_status -ne 0 ] || [ $bolt12_test_status -ne 0 ]; then
|
||||||
|
echo "Tests failed - happy_path_mint_wallet: $test_status, bolt12: $bolt12_test_status"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Exit with the status of the tests
|
# Exit with the status of the tests
|
||||||
exit $test_status
|
exit $test_status
|
||||||
|
|||||||
Reference in New Issue
Block a user