use std::convert::Infallible;
use std::pin::Pin;
use async_trait::async_trait;
use cashu::util::hex;
use cashu::{Bolt11Invoice, MeltOptions};
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
use futures::Stream;
use lightning::offers::offer::Offer;
use lightning_invoice::ParseOrSemanticError;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use crate::mint::{MeltPaymentRequest, MeltQuote};
use crate::nuts::{CurrencyUnit, MeltQuoteState};
use crate::Amount;
#[derive(Debug, Error)]
pub enum Error {
#[error("Invoice already paid")]
InvoiceAlreadyPaid,
#[error("Invoice pay is pending")]
InvoicePaymentPending,
#[error("Unsupported unit")]
UnsupportedUnit,
#[error("Unsupported payment option")]
UnsupportedPaymentOption,
#[error("Payment state is unknown")]
UnknownPaymentState,
#[error("Amount is not what is expected")]
AmountMismatch,
#[error(transparent)]
Lightning(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error(transparent)]
Parse(#[from] ParseOrSemanticError),
#[error(transparent)]
Amount(#[from] crate::amount::Error),
#[error(transparent)]
NUT04(#[from] crate::nuts::nut04::Error),
#[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error),
#[error(transparent)]
NUT23(#[from] crate::nuts::nut23::Error),
#[error("Hex error")]
Hex(#[from] hex::Error),
#[error("Invalid hash")]
InvalidHash,
#[error("`{0}`")]
Custom(String),
}
impl From<Infallible> for Error {
fn from(_: Infallible) -> Self {
unreachable!("Infallible cannot be constructed")
}
}
#[derive(Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum PaymentIdentifier {
Label(String),
OfferId(String),
PaymentHash([u8; 32]),
Bolt12PaymentHash([u8; 32]),
PaymentId([u8; 32]),
CustomId(String),
}
impl 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())),
"payment_id" => Ok(Self::PaymentId(
hex::decode(identifier)?
.try_into()
.map_err(|_| Error::InvalidHash)?,
)),
_ => Err(Error::UnsupportedPaymentOption),
}
}
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::PaymentId(_) => "payment_id".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::PaymentId(h) => write!(f, "{}", hex::encode(h)),
Self::CustomId(c) => write!(f, "{c}"),
}
}
}
impl std::fmt::Debug for PaymentIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PaymentIdentifier::PaymentHash(h) => write!(f, "PaymentHash({})", hex::encode(h)),
PaymentIdentifier::Bolt12PaymentHash(h) => {
write!(f, "Bolt12PaymentHash({})", hex::encode(h))
}
PaymentIdentifier::PaymentId(h) => write!(f, "PaymentId({})", hex::encode(h)),
PaymentIdentifier::Label(s) => write!(f, "Label({})", s),
PaymentIdentifier::OfferId(s) => write!(f, "OfferId({})", s),
PaymentIdentifier::CustomId(s) => write!(f, "CustomId({})", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bolt11IncomingPaymentOptions {
pub description: Option<String>,
pub amount: Amount<CurrencyUnit>,
pub unix_expiry: Option<u64>,
}
impl Default for Bolt11IncomingPaymentOptions {
fn default() -> Self {
Self {
description: None,
amount: Amount::new(0, CurrencyUnit::Sat),
unix_expiry: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct Bolt12IncomingPaymentOptions {
pub description: Option<String>,
pub amount: Option<Amount<CurrencyUnit>>,
pub unix_expiry: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomIncomingPaymentOptions {
pub method: String,
pub description: Option<String>,
pub amount: Amount<CurrencyUnit>,
pub unix_expiry: Option<u64>,
pub extra_json: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IncomingPaymentOptions {
Bolt11(Bolt11IncomingPaymentOptions),
Bolt12(Box<Bolt12IncomingPaymentOptions>),
Custom(Box<CustomIncomingPaymentOptions>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bolt11OutgoingPaymentOptions {
pub bolt11: Bolt11Invoice,
pub max_fee_amount: Option<Amount<CurrencyUnit>>,
pub timeout_secs: Option<u64>,
pub melt_options: Option<MeltOptions>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Bolt12OutgoingPaymentOptions {
pub offer: Offer,
pub max_fee_amount: Option<Amount<CurrencyUnit>>,
pub timeout_secs: Option<u64>,
pub melt_options: Option<MeltOptions>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomOutgoingPaymentOptions {
pub method: String,
pub request: String,
pub max_fee_amount: Option<Amount<CurrencyUnit>>,
pub timeout_secs: Option<u64>,
pub melt_options: Option<MeltOptions>,
pub extra_json: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OutgoingPaymentOptions {
Bolt11(Box<Bolt11OutgoingPaymentOptions>),
Bolt12(Box<Bolt12OutgoingPaymentOptions>),
Custom(Box<CustomOutgoingPaymentOptions>),
}
impl OutgoingPaymentOptions {
pub fn from_melt_quote_with_fee(
melt_quote: MeltQuote,
) -> Result<OutgoingPaymentOptions, Error> {
let fee_reserve = melt_quote.fee_reserve();
match &melt_quote.request {
MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
Bolt11OutgoingPaymentOptions {
max_fee_amount: Some(fee_reserve),
timeout_secs: None,
bolt11: bolt11.clone(),
melt_options: melt_quote.options,
},
))),
MeltPaymentRequest::Bolt12 { offer } => {
let melt_options = match melt_quote.options {
Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
Some(options) => Some(options),
_ => None,
};
Ok(OutgoingPaymentOptions::Bolt12(Box::new(
Bolt12OutgoingPaymentOptions {
max_fee_amount: Some(fee_reserve),
timeout_secs: None,
offer: *offer.clone(),
melt_options,
},
)))
}
MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
Box::new(CustomOutgoingPaymentOptions {
method: method.to_string(),
request: request.to_string(),
max_fee_amount: Some(fee_reserve),
timeout_secs: None,
melt_options: melt_quote.options,
extra_json: None,
}),
)),
}
}
}
#[async_trait]
pub trait MintPayment {
type Err: Into<Error> + From<Error>;
async fn start(&self) -> Result<(), Self::Err> {
Ok(())
}
async fn stop(&self) -> Result<(), Self::Err> {
Ok(())
}
async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
async fn create_incoming_payment_request(
&self,
options: IncomingPaymentOptions,
) -> Result<CreateIncomingPaymentResponse, Self::Err>;
async fn get_payment_quote(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<PaymentQuoteResponse, Self::Err>;
async fn make_payment(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<MakePaymentResponse, Self::Err>;
async fn wait_payment_event(
&self,
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
fn is_wait_invoice_active(&self) -> bool;
fn cancel_wait_invoice(&self);
async fn check_incoming_payment_status(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
async fn check_outgoing_payment(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<MakePaymentResponse, Self::Err>;
}
#[derive(Debug, Clone, Hash)]
pub enum Event {
PaymentReceived(WaitPaymentResponse),
}
impl Default for Event {
fn default() -> Self {
Event::PaymentReceived(WaitPaymentResponse {
payment_identifier: PaymentIdentifier::CustomId("default".to_string()),
payment_amount: Amount::new(0, CurrencyUnit::Msat),
payment_id: "default".to_string(),
})
}
}
#[derive(Debug, Clone, Hash)]
pub struct WaitPaymentResponse {
pub payment_identifier: PaymentIdentifier,
pub payment_amount: Amount<CurrencyUnit>,
pub payment_id: String,
}
impl WaitPaymentResponse {
pub fn unit(&self) -> &CurrencyUnit {
self.payment_amount.unit()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateIncomingPaymentResponse {
pub request_lookup_id: PaymentIdentifier,
pub request: String,
pub expiry: Option<u64>,
#[serde(flatten, default)]
pub extra_json: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct MakePaymentResponse {
pub payment_lookup_id: PaymentIdentifier,
pub payment_proof: Option<String>,
pub status: MeltQuoteState,
pub total_spent: Amount<CurrencyUnit>,
}
impl MakePaymentResponse {
pub fn unit(&self) -> &CurrencyUnit {
self.total_spent.unit()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PaymentQuoteResponse {
pub request_lookup_id: Option<PaymentIdentifier>,
pub amount: Amount<CurrencyUnit>,
pub fee: Amount<CurrencyUnit>,
pub state: MeltQuoteState,
}
impl PaymentQuoteResponse {
pub fn unit(&self) -> &CurrencyUnit {
self.amount.unit()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Bolt11Settings {
pub mpp: bool,
pub amountless: bool,
pub invoice_description: bool,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Bolt12Settings {
pub amountless: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsResponse {
pub unit: String,
pub bolt11: Option<Bolt11Settings>,
pub bolt12: Option<Bolt12Settings>,
#[serde(default)]
pub custom: std::collections::HashMap<String, String>,
}
impl From<SettingsResponse> for Value {
fn from(value: SettingsResponse) -> Self {
serde_json::to_value(value).unwrap_or(Value::Null)
}
}
impl TryFrom<Value> for SettingsResponse {
type Error = crate::error::Error;
fn try_from(value: Value) -> Result<Self, Self::Error> {
serde_json::from_value(value).map_err(|err| err.into())
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "prometheus")]
pub struct MetricsMintPayment<T> {
inner: T,
}
#[cfg(feature = "prometheus")]
impl<T> MetricsMintPayment<T>
where
T: MintPayment,
{
pub fn new(inner: T) -> Self {
Self { inner }
}
pub fn inner(&self) -> &T {
&self.inner
}
pub fn into_inner(self) -> T {
self.inner
}
}
#[async_trait]
#[cfg(feature = "prometheus")]
impl<T> MintPayment for MetricsMintPayment<T>
where
T: MintPayment + Send + Sync,
{
type Err = T::Err;
async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("get_settings");
let result = self.inner.get_settings().await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram("get_settings", result.is_ok(), duration);
METRICS.dec_in_flight_requests("get_settings");
result
}
async fn create_incoming_payment_request(
&self,
options: IncomingPaymentOptions,
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("create_incoming_payment_request");
let result = self.inner.create_incoming_payment_request(options).await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram(
"create_incoming_payment_request",
result.is_ok(),
duration,
);
METRICS.dec_in_flight_requests("create_incoming_payment_request");
result
}
async fn get_payment_quote(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<PaymentQuoteResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("get_payment_quote");
let result = self.inner.get_payment_quote(unit, options).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
if let Ok(ref quote) = result {
let amount: f64 = quote.amount.value() as f64;
let fee: f64 = quote.fee.value() as f64;
METRICS.record_lightning_payment(amount, fee);
}
METRICS.record_mint_operation_histogram("get_payment_quote", success, duration);
METRICS.dec_in_flight_requests("get_payment_quote");
result
}
async fn wait_payment_event(
&self,
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("wait_payment_event");
let result = self.inner.wait_payment_event().await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("wait_payment_event", success, duration);
METRICS.dec_in_flight_requests("wait_payment_event");
result
}
async fn make_payment(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<MakePaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("make_payment");
let result = self.inner.make_payment(unit, options).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("make_payment", success, duration);
METRICS.dec_in_flight_requests("make_payment");
result
}
fn is_wait_invoice_active(&self) -> bool {
self.inner.is_wait_invoice_active()
}
fn cancel_wait_invoice(&self) {
self.inner.cancel_wait_invoice()
}
async fn check_incoming_payment_status(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("check_incoming_payment_status");
let result = self
.inner
.check_incoming_payment_status(payment_identifier)
.await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram(
"check_incoming_payment_status",
result.is_ok(),
duration,
);
METRICS.dec_in_flight_requests("check_incoming_payment_status");
result
}
async fn check_outgoing_payment(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<MakePaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("check_outgoing_payment");
let result = self.inner.check_outgoing_payment(payment_identifier).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("check_outgoing_payment", success, duration);
METRICS.dec_in_flight_requests("check_outgoing_payment");
result
}
}
pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;