1use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5
6use crate::nuts::nut00::KnownMethod;
7use crate::nuts::nut04::{MintQuoteCustomRequest, MintQuoteCustomResponse};
8use crate::nuts::nut23::{MintQuoteBolt11Request, MintQuoteBolt11Response, QuoteState};
9use crate::nuts::nut25::{MintQuoteBolt12Request, MintQuoteBolt12Response};
10use crate::nuts::nut30::{MintQuoteOnchainRequest, MintQuoteOnchainResponse};
11use crate::{Amount, CurrencyUnit, PaymentMethod, PublicKey};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum MintQuoteRequest {
16 Bolt11(MintQuoteBolt11Request),
18 Bolt12(MintQuoteBolt12Request),
20 Onchain(MintQuoteOnchainRequest),
22 Custom {
24 method: PaymentMethod,
26 request: MintQuoteCustomRequest,
28 },
29}
30
31impl From<MintQuoteBolt11Request> for MintQuoteRequest {
32 fn from(request: MintQuoteBolt11Request) -> Self {
33 MintQuoteRequest::Bolt11(request)
34 }
35}
36
37impl From<MintQuoteBolt12Request> for MintQuoteRequest {
38 fn from(request: MintQuoteBolt12Request) -> Self {
39 MintQuoteRequest::Bolt12(request)
40 }
41}
42
43impl From<MintQuoteOnchainRequest> for MintQuoteRequest {
44 fn from(request: MintQuoteOnchainRequest) -> Self {
45 MintQuoteRequest::Onchain(request)
46 }
47}
48
49impl MintQuoteRequest {
50 pub fn method(&self) -> PaymentMethod {
52 match self {
53 Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
54 Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
55 Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
56 Self::Custom { method, .. } => method.clone(),
57 }
58 }
59
60 pub fn amount(&self) -> Option<Amount> {
62 match self {
63 Self::Bolt11(request) => Some(request.amount),
64 Self::Bolt12(request) => request.amount,
65 Self::Onchain(_) => None,
66 Self::Custom { request, .. } => request.amount,
67 }
68 }
69
70 pub fn unit(&self) -> CurrencyUnit {
72 match self {
73 Self::Bolt11(request) => request.unit.clone(),
74 Self::Bolt12(request) => request.unit.clone(),
75 Self::Onchain(request) => request.unit.clone(),
76 Self::Custom { request, .. } => request.unit.clone(),
77 }
78 }
79
80 pub fn payment_method(&self) -> PaymentMethod {
82 self.method()
83 }
84
85 pub fn pubkey(&self) -> Option<PublicKey> {
87 match self {
88 Self::Bolt11(request) => request.pubkey,
89 Self::Bolt12(request) => Some(request.pubkey),
90 Self::Onchain(request) => Some(request.pubkey),
91 Self::Custom { request, .. } => request.pubkey,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(bound = "Q: Serialize + DeserializeOwned")]
99pub enum MintQuoteResponse<Q> {
100 Bolt11(MintQuoteBolt11Response<Q>),
102 Bolt12(MintQuoteBolt12Response<Q>),
104 Onchain(MintQuoteOnchainResponse<Q>),
106 Custom {
108 method: PaymentMethod,
110 response: MintQuoteCustomResponse<Q>,
112 },
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
117pub enum MintQuoteAccountingError {
118 #[error("mint quote amount_issued ({amount_issued}) exceeds amount_paid ({amount_paid})")]
120 AmountIssuedExceedsAmountPaid {
121 amount_paid: Amount,
123 amount_issued: Amount,
125 },
126}
127
128impl<Q> MintQuoteResponse<Q> {
129 pub fn method(&self) -> PaymentMethod {
131 match self {
132 Self::Bolt11(_) => PaymentMethod::Known(KnownMethod::Bolt11),
133 Self::Bolt12(_) => PaymentMethod::Known(KnownMethod::Bolt12),
134 Self::Onchain(_) => PaymentMethod::Known(KnownMethod::Onchain),
135 Self::Custom { method, .. } => method.clone(),
136 }
137 }
138
139 pub fn quote(&self) -> &Q {
141 match self {
142 Self::Bolt11(r) => &r.quote,
143 Self::Bolt12(r) => &r.quote,
144 Self::Onchain(r) => &r.quote,
145 Self::Custom { response: r, .. } => &r.quote,
146 }
147 }
148
149 pub fn request(&self) -> &str {
151 match self {
152 Self::Bolt11(r) => &r.request,
153 Self::Bolt12(r) => &r.request,
154 Self::Onchain(r) => &r.request,
155 Self::Custom { response: r, .. } => &r.request,
156 }
157 }
158
159 pub fn state(&self) -> Option<QuoteState> {
161 self.try_state().ok()
162 }
163
164 pub fn try_state(&self) -> Result<QuoteState, MintQuoteAccountingError> {
166 match self {
167 Self::Bolt11(r) => {
168 if r.amount_paid > Amount::ZERO || r.amount_issued > Amount::ZERO {
169 quote_state_from_amounts(r.amount_paid, r.amount_issued)
170 } else {
171 Ok(r.state)
172 }
173 }
174 Self::Bolt12(r) => quote_state_from_amounts(r.amount_paid, r.amount_issued),
175 Self::Onchain(r) => quote_state_from_amounts(r.amount_paid, r.amount_issued),
176 Self::Custom { response, .. } => {
177 quote_state_from_amounts(response.amount_paid, response.amount_issued)
178 }
179 }
180 }
181
182 pub fn expiry(&self) -> Option<u64> {
184 match self {
185 Self::Bolt11(r) => r.expiry,
186 Self::Bolt12(r) => r.expiry,
187 Self::Onchain(r) => r.expiry,
188 Self::Custom { response: r, .. } => r.expiry,
189 }
190 }
191}
192
193pub fn quote_state_from_amounts(
195 amount_paid: Amount,
196 amount_issued: Amount,
197) -> Result<QuoteState, MintQuoteAccountingError> {
198 if amount_issued > amount_paid {
199 return Err(MintQuoteAccountingError::AmountIssuedExceedsAmountPaid {
200 amount_paid,
201 amount_issued,
202 });
203 }
204
205 if amount_paid == Amount::ZERO && amount_issued == Amount::ZERO {
206 return Ok(QuoteState::Unpaid);
207 }
208
209 if amount_paid == amount_issued {
210 return Ok(QuoteState::Issued);
211 }
212
213 Ok(QuoteState::Paid)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn custom_response(amount_paid: Amount, amount_issued: Amount) -> MintQuoteResponse<String> {
221 MintQuoteResponse::Custom {
222 method: PaymentMethod::Custom("custom".to_string()),
223 response: MintQuoteCustomResponse {
224 quote: "quote".to_string(),
225 request: "custom-request".to_string(),
226 method: PaymentMethod::Custom("custom".to_string()),
227 amount: Some(Amount::from(100)),
228 amount_paid,
229 amount_issued,
230 updated_at: 0,
231 unit: Some(CurrencyUnit::Sat),
232 expiry: None,
233 pubkey: None,
234 extra: serde_json::Value::Null,
235 },
236 }
237 }
238
239 #[test]
240 fn custom_state_is_derived_from_amount_counters() {
241 assert_eq!(
242 custom_response(Amount::ZERO, Amount::ZERO).state(),
243 Some(QuoteState::Unpaid)
244 );
245 assert_eq!(
246 custom_response(Amount::from(100), Amount::ZERO).state(),
247 Some(QuoteState::Paid)
248 );
249 assert_eq!(
250 custom_response(Amount::from(100), Amount::from(100)).state(),
251 Some(QuoteState::Issued)
252 );
253 assert_eq!(
254 custom_response(Amount::from(50), Amount::from(100)).state(),
255 None
256 );
257 assert!(matches!(
258 custom_response(Amount::from(50), Amount::from(100)).try_state(),
259 Err(MintQuoteAccountingError::AmountIssuedExceedsAmountPaid { .. })
260 ));
261 }
262
263 #[test]
264 fn bolt12_state_uses_unissued_amount() {
265 let response = MintQuoteResponse::Bolt12(MintQuoteBolt12Response {
266 quote: "quote".to_string(),
267 request: "bolt12-request".to_string(),
268 amount: Some(Amount::from(100)),
269 unit: CurrencyUnit::Sat,
270 method: PaymentMethod::Known(KnownMethod::Bolt12),
271 expiry: None,
272 pubkey: PublicKey::from_hex(
273 "02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c",
274 )
275 .expect("valid public key"),
276 amount_paid: Amount::from(100),
277 amount_issued: Amount::from(40),
278 updated_at: 0,
279 });
280
281 assert_eq!(response.state(), Some(QuoteState::Paid));
282 }
283}