use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::nuts::nut00::KnownMethod;
use crate::nuts::nut05::{MeltQuoteCustomRequest, MeltQuoteCustomResponse};
use crate::nuts::nut23::{MeltQuoteBolt11Request, MeltQuoteBolt11Response};
use crate::nuts::nut25::{MeltQuoteBolt12Request, MeltQuoteBolt12Response};
use crate::nuts::nut_onchain::{MeltQuoteOnchainRequest, MeltQuoteOnchainResponse};
use crate::{Amount, CurrencyUnit, MeltQuoteState, PaymentMethod};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MeltQuoteRequest {
Bolt11(MeltQuoteBolt11Request),
Bolt12(MeltQuoteBolt12Request),
Onchain(MeltQuoteOnchainRequest),
Custom(MeltQuoteCustomRequest),
}
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)
}
}
impl From<MeltQuoteOnchainRequest> for MeltQuoteRequest {
fn from(request: MeltQuoteOnchainRequest) -> Self {
MeltQuoteRequest::Onchain(request)
}
}
impl From<MeltQuoteCustomRequest> for MeltQuoteRequest {
fn from(request: MeltQuoteCustomRequest) -> Self {
MeltQuoteRequest::Custom(request)
}
}
impl MeltQuoteRequest {
pub fn method(&self) -> PaymentMethod {
match self {
Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
Self::Custom(request) => PaymentMethod::from(request.method.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub enum MeltQuoteResponse<Q> {
Bolt11(MeltQuoteBolt11Response<Q>),
Bolt12(MeltQuoteBolt12Response<Q>),
Onchain(MeltQuoteOnchainResponse<Q>),
Custom((PaymentMethod, MeltQuoteCustomResponse<Q>)),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "Q: Serialize + DeserializeOwned")]
pub enum MeltQuoteCreateResponse<Q> {
Bolt11(MeltQuoteBolt11Response<Q>),
Bolt12(MeltQuoteBolt12Response<Q>),
Onchain(MeltQuoteOnchainResponse<Q>),
Custom((PaymentMethod, MeltQuoteCustomResponse<Q>)),
}
impl<Q> MeltQuoteResponse<Q> {
pub fn method(&self) -> PaymentMethod {
match self {
Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
Self::Custom((method, _)) => method.clone(),
}
}
}
impl<Q: ToString> MeltQuoteResponse<Q> {
pub fn to_string_id(self) -> MeltQuoteResponse<String> {
match self {
Self::Bolt11(r) => MeltQuoteResponse::Bolt11(r.to_string_id()),
Self::Bolt12(r) => MeltQuoteResponse::Bolt12(r.to_string_id()),
Self::Onchain(r) => MeltQuoteResponse::Onchain(r.to_string_id()),
Self::Custom((method, r)) => MeltQuoteResponse::Custom((method, r.to_string_id())),
}
}
pub fn quote(&self) -> &Q {
match self {
Self::Bolt11(r) => &r.quote,
Self::Bolt12(r) => &r.quote,
Self::Onchain(r) => &r.quote,
Self::Custom((_, r)) => &r.quote,
}
}
pub fn amount(&self) -> Amount {
match self {
Self::Bolt11(r) => r.amount,
Self::Bolt12(r) => r.amount,
Self::Onchain(r) => r.amount,
Self::Custom((_, r)) => r.amount,
}
}
pub fn fee_reserve(&self) -> Amount {
match self {
Self::Bolt11(r) => r.fee_reserve,
Self::Bolt12(r) => r.fee_reserve,
Self::Onchain(r) => r
.selected_fee_index
.and_then(|selected| {
r.fee_options
.iter()
.find(|option| option.fee_index == selected)
})
.or_else(|| r.fee_options.first())
.map(|option| option.fee_reserve)
.unwrap_or(Amount::ZERO),
Self::Custom((_, r)) => r.fee_reserve.unwrap_or_default(),
}
}
pub fn state(&self) -> MeltQuoteState {
match self {
Self::Bolt11(r) => r.state,
Self::Bolt12(r) => r.state,
Self::Onchain(r) => r.state,
Self::Custom((_, r)) => r.state,
}
}
pub fn expiry(&self) -> u64 {
match self {
Self::Bolt11(r) => r.expiry,
Self::Bolt12(r) => r.expiry,
Self::Onchain(r) => r.expiry,
Self::Custom((_, r)) => r.expiry,
}
}
pub fn payment_proof(&self) -> Option<&str> {
match self {
Self::Bolt11(r) => r.payment_preimage.as_deref(),
Self::Bolt12(r) => r.payment_preimage.as_deref(),
Self::Onchain(r) => r.outpoint.as_deref(),
Self::Custom((_, r)) => r.payment_preimage.as_deref(),
}
}
pub fn change(&self) -> Option<&Vec<crate::BlindSignature>> {
match self {
Self::Bolt11(r) => r.change.as_ref(),
Self::Bolt12(r) => r.change.as_ref(),
Self::Onchain(r) => r.change.as_ref(),
Self::Custom((_, r)) => r.change.as_ref(),
}
}
pub fn request(&self) -> Option<&str> {
match self {
Self::Bolt11(r) => r.request.as_deref(),
Self::Bolt12(r) => r.request.as_deref(),
Self::Onchain(r) => Some(r.request.as_str()),
Self::Custom((_, r)) => r.request.as_deref(),
}
}
pub fn unit(&self) -> Option<CurrencyUnit> {
match self {
Self::Bolt11(r) => r.unit.clone(),
Self::Bolt12(r) => r.unit.clone(),
Self::Onchain(r) => Some(r.unit.clone()),
Self::Custom((_, r)) => r.unit.clone(),
}
}
}
#[cfg(feature = "mint")]
impl From<MeltQuoteResponse<crate::QuoteId>> for MeltQuoteResponse<String> {
fn from(value: MeltQuoteResponse<crate::QuoteId>) -> Self {
value.to_string_id()
}
}
impl<Q: ToString> MeltQuoteCreateResponse<Q> {
pub fn to_string_id(self) -> MeltQuoteCreateResponse<String> {
match self {
Self::Bolt11(r) => MeltQuoteCreateResponse::Bolt11(r.to_string_id()),
Self::Bolt12(r) => MeltQuoteCreateResponse::Bolt12(r.to_string_id()),
Self::Onchain(r) => MeltQuoteCreateResponse::Onchain(r.to_string_id()),
Self::Custom((method, r)) => {
MeltQuoteCreateResponse::Custom((method, r.to_string_id()))
}
}
}
pub fn method(&self) -> PaymentMethod {
match self {
Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
Self::Custom((method, _)) => method.clone(),
}
}
pub fn quote(&self) -> Option<&Q> {
match self {
Self::Bolt11(r) => Some(&r.quote),
Self::Bolt12(r) => Some(&r.quote),
Self::Onchain(r) => Some(&r.quote),
Self::Custom((_, r)) => Some(&r.quote),
}
}
}
#[cfg(feature = "mint")]
impl From<MeltQuoteCreateResponse<crate::QuoteId>> for MeltQuoteCreateResponse<String> {
fn from(value: MeltQuoteCreateResponse<crate::QuoteId>) -> Self {
value.to_string_id()
}
}
#[cfg(feature = "mint")]
impl<Q> From<crate::mint::MeltQuote> for MeltQuoteResponse<Q>
where
Q: From<crate::QuoteId>,
{
fn from(value: crate::mint::MeltQuote) -> Self {
match value.payment_method {
PaymentMethod::Known(KnownMethod::Bolt11) => {
Self::Bolt11(crate::nuts::nut23::MeltQuoteBolt11Response {
quote: value.id.clone().into(),
amount: value.amount().into(),
fee_reserve: value.fee_reserve().into(),
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_proof.clone(),
change: None,
request: Some(value.request.to_string()),
unit: Some(value.unit.clone()),
})
}
PaymentMethod::Known(KnownMethod::Bolt12) => {
Self::Bolt12(crate::nuts::nut25::MeltQuoteBolt12Response {
quote: value.id.clone().into(),
amount: value.amount().into(),
fee_reserve: value.fee_reserve().into(),
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_proof.clone(),
change: None,
request: Some(value.request.to_string()),
unit: Some(value.unit.clone()),
})
}
PaymentMethod::Known(KnownMethod::Onchain) => {
Self::Onchain(crate::nuts::nut_onchain::MeltQuoteOnchainResponse {
quote: value.id.clone().into(),
amount: value.amount().into(),
unit: value.unit.clone(),
state: value.state,
expiry: value.expiry,
request: value.request.to_string(),
fee_options: value.fee_options().to_vec(),
selected_fee_index: value.selected_fee_index,
outpoint: value.payment_proof.clone(),
change: None,
})
}
ref method => Self::Custom((
method.clone(),
crate::nuts::nut05::MeltQuoteCustomResponse {
quote: value.id.clone().into(),
amount: value.amount().into(),
fee_reserve: Some(value.fee_reserve().into()),
state: value.state,
expiry: value.expiry,
payment_preimage: value.payment_proof.clone(),
change: None,
request: Some(value.request.to_string()),
unit: Some(value.unit.clone()),
extra: serde_json::Value::Null,
},
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nuts::nut05::MeltQuoteCustomResponse;
use crate::nuts::nut23::MeltQuoteBolt11Response;
use crate::nuts::nut_onchain::{MeltQuoteOnchainFeeOption, MeltQuoteOnchainResponse};
use crate::{Amount, CurrencyUnit, MeltQuoteState};
fn bolt11_response(quote: &str) -> MeltQuoteBolt11Response<String> {
MeltQuoteBolt11Response {
quote: quote.to_string(),
amount: Amount::from(100),
fee_reserve: Amount::from(1),
state: MeltQuoteState::Unpaid,
expiry: 1000,
payment_preimage: Some("preimage-11".to_string()),
change: None,
request: Some("lnbc100".to_string()),
unit: Some(CurrencyUnit::Sat),
}
}
fn bolt12_response(quote: &str) -> MeltQuoteBolt12Response<String> {
MeltQuoteBolt12Response {
quote: quote.to_string(),
amount: Amount::from(200),
fee_reserve: Amount::from(2),
state: MeltQuoteState::Pending,
expiry: 2000,
payment_preimage: Some("preimage-12".to_string()),
change: None,
request: Some("lno200".to_string()),
unit: Some(CurrencyUnit::Sat),
}
}
fn onchain_response(quote: &str) -> MeltQuoteOnchainResponse<String> {
MeltQuoteOnchainResponse {
quote: quote.to_string(),
amount: Amount::from(400),
unit: CurrencyUnit::Sat,
state: MeltQuoteState::Paid,
expiry: 4000,
request: "bc1qonchainaddress".to_string(),
fee_options: vec![MeltQuoteOnchainFeeOption {
fee_index: 0,
fee_reserve: Amount::from(4),
estimated_blocks: 6,
}],
selected_fee_index: Some(0),
outpoint: Some("abcd...ef:0".to_string()),
change: None,
}
}
fn custom_response(quote: &str) -> MeltQuoteCustomResponse<String> {
MeltQuoteCustomResponse {
quote: quote.to_string(),
amount: Amount::from(300),
fee_reserve: Some(Amount::from(3)),
state: MeltQuoteState::Paid,
expiry: 3000,
payment_preimage: Some("outpoint-abc".to_string()),
change: None,
request: Some("bc1qaddress".to_string()),
unit: Some(CurrencyUnit::Sat),
extra: serde_json::Value::Null,
}
}
#[test]
fn melt_quote_response_accessors_bolt11() {
let r = MeltQuoteResponse::Bolt11(bolt11_response("q11"));
assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Bolt11));
assert_eq!(r.quote(), "q11");
assert_eq!(r.amount(), Amount::from(100));
assert_eq!(r.fee_reserve(), Amount::from(1));
assert_eq!(r.state(), MeltQuoteState::Unpaid);
assert_eq!(r.expiry(), 1000);
assert_eq!(r.payment_proof(), Some("preimage-11"));
assert!(r.change().is_none());
assert_eq!(r.request(), Some("lnbc100"));
assert_eq!(r.unit(), Some(CurrencyUnit::Sat));
}
#[test]
fn melt_quote_response_accessors_bolt12() {
let r = MeltQuoteResponse::Bolt12(bolt12_response("q12"));
assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Bolt12));
assert_eq!(r.quote(), "q12");
assert_eq!(r.amount(), Amount::from(200));
assert_eq!(r.fee_reserve(), Amount::from(2));
assert_eq!(r.state(), MeltQuoteState::Pending);
assert_eq!(r.expiry(), 2000);
assert_eq!(r.payment_proof(), Some("preimage-12"));
assert_eq!(r.request(), Some("lno200"));
}
#[test]
fn melt_quote_response_accessors_onchain() {
let r = MeltQuoteResponse::Onchain(onchain_response("qoc"));
assert_eq!(r.method(), PaymentMethod::Known(KnownMethod::Onchain));
assert_eq!(r.quote(), "qoc");
assert_eq!(r.amount(), Amount::from(400));
assert_eq!(r.fee_reserve(), Amount::from(4));
assert_eq!(r.state(), MeltQuoteState::Paid);
assert_eq!(r.expiry(), 4000);
assert_eq!(r.payment_proof(), Some("abcd...ef:0"));
assert!(r.change().is_none());
assert_eq!(r.request(), Some("bc1qonchainaddress"));
assert_eq!(r.unit(), Some(CurrencyUnit::Sat));
}
#[test]
fn melt_quote_response_accessors_custom() {
let method = PaymentMethod::from("paypal");
let r = MeltQuoteResponse::Custom((method.clone(), custom_response("qc")));
assert_eq!(r.method(), method);
assert_eq!(r.quote(), "qc");
assert_eq!(r.amount(), Amount::from(300));
assert_eq!(r.fee_reserve(), Amount::from(3));
assert_eq!(r.state(), MeltQuoteState::Paid);
assert_eq!(r.expiry(), 3000);
assert_eq!(r.payment_proof(), Some("outpoint-abc"));
assert_eq!(r.request(), Some("bc1qaddress"));
}
#[test]
fn melt_quote_create_response_accessors() {
let r11 = MeltQuoteCreateResponse::Bolt11(bolt11_response("c11"));
assert_eq!(r11.method(), PaymentMethod::Known(KnownMethod::Bolt11));
assert_eq!(r11.quote().map(String::as_str), Some("c11"));
let r12 = MeltQuoteCreateResponse::Bolt12(bolt12_response("c12"));
assert_eq!(r12.method(), PaymentMethod::Known(KnownMethod::Bolt12));
assert_eq!(r12.quote().map(String::as_str), Some("c12"));
let method = PaymentMethod::from("venmo");
let rc = MeltQuoteCreateResponse::Custom((method.clone(), custom_response("cc")));
assert_eq!(rc.method(), method);
assert_eq!(rc.quote().map(String::as_str), Some("cc"));
}
#[test]
fn melt_quote_request_method_dispatch() {
use crate::nuts::nut05::MeltQuoteCustomRequest;
let custom_req = MeltQuoteCustomRequest {
method: "cashapp".to_string(),
unit: CurrencyUnit::Sat,
request: "$tag".to_string(),
extra: serde_json::Value::Null,
};
let req: MeltQuoteRequest = custom_req.into();
assert_eq!(req.method(), PaymentMethod::from("cashapp"));
}
}