Skip to main content

cdk_ffi/types/
quote.rs

1//! Quote-related FFI types
2
3use serde::{Deserialize, Serialize};
4
5use super::amount::{Amount, CurrencyUnit};
6use super::mint::MintUrl;
7use crate::error::FfiError;
8
9/// FFI-compatible MintQuote
10#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
11pub struct MintQuote {
12    /// Quote ID
13    pub id: String,
14    /// Requested or fixed quote amount, when defined by the payment method.
15    ///
16    /// Variable-amount methods such as onchain leave this unset and track
17    /// funds through `amount_paid` and `amount_issued`.
18    pub amount: Option<Amount>,
19    /// Currency unit
20    pub unit: CurrencyUnit,
21    /// Payment request
22    pub request: String,
23    /// Quote state
24    pub state: QuoteState,
25    /// Expiry timestamp
26    pub expiry: u64,
27    /// Mint URL
28    pub mint_url: MintUrl,
29    /// Amount issued
30    pub amount_issued: Amount,
31    /// Amount paid
32    pub amount_paid: Amount,
33    /// Estimated confirmation target in blocks for onchain quotes
34    pub estimated_blocks: Option<u32>,
35    /// Payment method
36    pub payment_method: PaymentMethod,
37    /// Secret key (optional, hex-encoded)
38    pub secret_key: Option<String>,
39    /// Operation ID that reserved this quote
40    pub used_by_operation: Option<String>,
41    /// Version for optimistic locking
42    #[serde(default)]
43    pub version: u32,
44}
45
46impl From<cdk::wallet::MintQuote> for MintQuote {
47    fn from(quote: cdk::wallet::MintQuote) -> Self {
48        Self {
49            id: quote.id.clone(),
50            amount: quote.amount.map(Into::into),
51            unit: quote.unit.clone().into(),
52            request: quote.request.clone(),
53            state: quote.state.into(),
54            expiry: quote.expiry,
55            mint_url: quote.mint_url.clone().into(),
56            amount_issued: quote.amount_issued.into(),
57            amount_paid: quote.amount_paid.into(),
58            estimated_blocks: quote.estimated_blocks,
59            payment_method: quote.payment_method.into(),
60            secret_key: quote.secret_key.map(|sk| sk.to_secret_hex()),
61            used_by_operation: quote.used_by_operation.map(|id| id.to_string()),
62            version: quote.version,
63        }
64    }
65}
66
67impl TryFrom<MintQuote> for cdk::wallet::MintQuote {
68    type Error = FfiError;
69
70    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
71        let secret_key = quote
72            .secret_key
73            .map(|hex| cdk::nuts::SecretKey::from_hex(&hex))
74            .transpose()
75            .map_err(|e| FfiError::internal(format!("Invalid secret key: {}", e)))?;
76
77        Ok(Self {
78            id: quote.id,
79            amount: quote.amount.map(Into::into),
80            unit: quote.unit.into(),
81            request: quote.request,
82            state: quote.state.into(),
83            expiry: quote.expiry,
84            mint_url: quote.mint_url.try_into()?,
85            amount_issued: quote.amount_issued.into(),
86            amount_paid: quote.amount_paid.into(),
87            estimated_blocks: quote.estimated_blocks,
88            payment_method: quote.payment_method.into(),
89            secret_key,
90            used_by_operation: quote.used_by_operation,
91            version: quote.version,
92        })
93    }
94}
95
96/// Get total amount for a mint quote (amount paid)
97#[uniffi::export]
98pub fn mint_quote_total_amount(quote: &MintQuote) -> Result<Amount, FfiError> {
99    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
100    Ok(cdk_quote.total_amount().into())
101}
102
103/// Check if mint quote is expired
104#[uniffi::export]
105pub fn mint_quote_is_expired(quote: &MintQuote, current_time: u64) -> Result<bool, FfiError> {
106    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
107    Ok(cdk_quote.is_expired(current_time))
108}
109
110/// Get amount that can be minted from a mint quote
111#[uniffi::export]
112pub fn mint_quote_amount_mintable(quote: &MintQuote) -> Result<Amount, FfiError> {
113    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
114    Ok(cdk_quote.amount_mintable().into())
115}
116
117/// Decode MintQuote from JSON string
118#[uniffi::export]
119pub fn decode_mint_quote(json: String) -> Result<MintQuote, FfiError> {
120    let quote: cdk::wallet::MintQuote = serde_json::from_str(&json)?;
121    Ok(quote.into())
122}
123
124/// Encode MintQuote to JSON string
125#[uniffi::export]
126pub fn encode_mint_quote(quote: MintQuote) -> Result<String, FfiError> {
127    Ok(serde_json::to_string(&quote)?)
128}
129
130/// FFI-compatible MintQuoteBolt11Response
131#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
132pub struct MintQuoteBolt11Response {
133    /// Quote ID
134    pub quote: String,
135    /// Request string
136    pub request: String,
137    /// State of the quote
138    pub state: QuoteState,
139    /// Expiry timestamp (optional)
140    pub expiry: Option<u64>,
141    /// Amount (optional)
142    pub amount: Option<Amount>,
143    /// Unit (optional)
144    pub unit: Option<CurrencyUnit>,
145    /// Pubkey (optional)
146    pub pubkey: Option<String>,
147}
148
149impl From<cdk::nuts::MintQuoteBolt11Response<String>> for MintQuoteBolt11Response {
150    fn from(response: cdk::nuts::MintQuoteBolt11Response<String>) -> Self {
151        Self {
152            quote: response.quote,
153            request: response.request,
154            state: response.state.into(),
155            expiry: response.expiry,
156            amount: response.amount.map(Into::into),
157            unit: response.unit.map(Into::into),
158            pubkey: response.pubkey.map(|p| p.to_string()),
159        }
160    }
161}
162
163impl From<cdk::wallet::MintQuote> for MintQuoteBolt11Response {
164    fn from(quote: cdk::wallet::MintQuote) -> Self {
165        Self {
166            quote: quote.id,
167            request: quote.request,
168            state: quote.state.into(),
169            expiry: Some(quote.expiry),
170            amount: quote.amount.map(Into::into),
171            unit: Some(quote.unit.into()),
172            pubkey: quote.secret_key.map(|sk| sk.public_key().to_string()),
173        }
174    }
175}
176
177/// FFI-compatible MintQuoteCustomResponse
178///
179/// This is a unified response type for custom payment methods that includes
180/// extra fields for method-specific data (e.g., ehash share).
181#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
182pub struct MintQuoteCustomResponse {
183    /// Quote ID
184    pub quote: String,
185    /// Request string
186    pub request: String,
187    /// Expiry timestamp (optional)
188    pub expiry: Option<u64>,
189    /// Amount (optional)
190    pub amount: Option<Amount>,
191    /// Amount paid
192    pub amount_paid: Amount,
193    /// Amount issued
194    pub amount_issued: Amount,
195    /// Unit (optional)
196    pub unit: Option<CurrencyUnit>,
197    /// Pubkey (optional)
198    pub pubkey: Option<String>,
199    /// Extra payment-method-specific fields as JSON string
200    ///
201    /// These fields are flattened into the JSON representation, allowing
202    /// custom payment methods to include additional data without nesting.
203    pub extra: Option<String>,
204}
205
206impl From<cdk::nuts::MintQuoteCustomResponse<String>> for MintQuoteCustomResponse {
207    fn from(response: cdk::nuts::MintQuoteCustomResponse<String>) -> Self {
208        let extra = if response.extra.is_null() {
209            None
210        } else {
211            Some(response.extra.to_string())
212        };
213
214        Self {
215            quote: response.quote,
216            request: response.request,
217            expiry: response.expiry,
218            amount: response.amount.map(Into::into),
219            amount_paid: response.amount_paid.into(),
220            amount_issued: response.amount_issued.into(),
221            unit: response.unit.map(Into::into),
222            pubkey: response.pubkey.map(|p| p.to_string()),
223            extra,
224        }
225    }
226}
227
228/// FFI-compatible MeltQuoteBolt11Response
229#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
230pub struct MeltQuoteBolt11Response {
231    /// Quote ID
232    pub quote: String,
233    /// Amount
234    pub amount: Amount,
235    /// Fee reserve
236    pub fee_reserve: Amount,
237    /// State of the quote
238    pub state: QuoteState,
239    /// Expiry timestamp
240    pub expiry: u64,
241    /// Payment proof (optional)
242    pub payment_proof: Option<String>,
243    /// Request string (optional)
244    pub request: Option<String>,
245    /// Unit (optional)
246    pub unit: Option<CurrencyUnit>,
247}
248
249impl From<cdk::nuts::MeltQuoteBolt11Response<String>> for MeltQuoteBolt11Response {
250    fn from(response: cdk::nuts::MeltQuoteBolt11Response<String>) -> Self {
251        Self {
252            quote: response.quote,
253            amount: response.amount.into(),
254            fee_reserve: response.fee_reserve.into(),
255            state: response.state.into(),
256            expiry: response.expiry,
257            payment_proof: response.payment_preimage,
258            request: response.request,
259            unit: response.unit.map(Into::into),
260        }
261    }
262}
263
264/// FFI-compatible MeltQuoteCustomResponse
265///
266/// This is a unified response type for custom payment methods that includes
267/// extra fields for method-specific data.
268#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
269pub struct MeltQuoteCustomResponse {
270    /// Quote ID
271    pub quote: String,
272    /// Amount
273    pub amount: Amount,
274    /// Fee reserve
275    pub fee_reserve: Option<Amount>,
276    /// State of the quote
277    pub state: QuoteState,
278    /// Expiry timestamp
279    pub expiry: u64,
280    /// Payment proof (optional)
281    pub payment_proof: Option<String>,
282    /// Request string (optional)
283    pub request: Option<String>,
284    /// Unit (optional)
285    pub unit: Option<CurrencyUnit>,
286    /// Extra payment-method-specific fields as JSON string
287    ///
288    /// These fields are flattened into the JSON representation, allowing
289    /// custom payment methods to include additional data without nesting.
290    pub extra: Option<String>,
291}
292
293impl From<cdk::nuts::MeltQuoteCustomResponse<String>> for MeltQuoteCustomResponse {
294    fn from(response: cdk::nuts::MeltQuoteCustomResponse<String>) -> Self {
295        let extra = if response.extra.is_null() {
296            None
297        } else {
298            Some(response.extra.to_string())
299        };
300
301        Self {
302            quote: response.quote,
303            amount: response.amount.into(),
304            fee_reserve: response.fee_reserve.map(Into::into),
305            state: response.state.into(),
306            expiry: response.expiry,
307            payment_proof: response.payment_preimage,
308            request: response.request,
309            unit: response.unit.map(Into::into),
310            extra,
311        }
312    }
313}
314
315/// FFI-compatible PaymentMethod
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
317pub enum PaymentMethod {
318    /// Bolt11 payment type
319    Bolt11,
320    /// Bolt12 payment type
321    Bolt12,
322    /// Onchain Bitcoin payment type
323    Onchain,
324    /// Custom payment type
325    Custom { method: String },
326}
327
328impl From<cdk::nuts::PaymentMethod> for PaymentMethod {
329    fn from(method: cdk::nuts::PaymentMethod) -> Self {
330        match method.as_str() {
331            "bolt11" => Self::Bolt11,
332            "bolt12" => Self::Bolt12,
333            "onchain" => Self::Onchain,
334            s => Self::Custom {
335                method: s.to_string(),
336            },
337        }
338    }
339}
340
341impl From<PaymentMethod> for cdk::nuts::PaymentMethod {
342    fn from(method: PaymentMethod) -> Self {
343        match method {
344            PaymentMethod::Bolt11 => Self::from("bolt11"),
345            PaymentMethod::Bolt12 => Self::from("bolt12"),
346            PaymentMethod::Onchain => Self::from("onchain"),
347            PaymentMethod::Custom { method } => Self::from(method),
348        }
349    }
350}
351
352/// FFI-compatible MintQuoteOnchainResponse.
353#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
354pub struct MintQuoteOnchainResponse {
355    /// Quote ID
356    pub quote: String,
357    /// Bitcoin address to pay
358    pub request: String,
359    /// Unit
360    pub unit: CurrencyUnit,
361    /// Expiry timestamp
362    pub expiry: Option<u64>,
363    /// NUT-20 public key
364    pub pubkey: String,
365    /// Total confirmed amount paid to the onchain address
366    pub amount_paid: Amount,
367    /// Amount already issued for this quote
368    pub amount_issued: Amount,
369}
370
371impl From<cdk::nuts::MintQuoteOnchainResponse<String>> for MintQuoteOnchainResponse {
372    fn from(response: cdk::nuts::MintQuoteOnchainResponse<String>) -> Self {
373        Self {
374            quote: response.quote,
375            request: response.request,
376            unit: response.unit.into(),
377            expiry: response.expiry,
378            pubkey: response.pubkey.to_string(),
379            amount_paid: response.amount_paid.into(),
380            amount_issued: response.amount_issued.into(),
381        }
382    }
383}
384
385/// Fee option for an onchain melt quote.
386#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
387pub struct MeltQuoteOnchainFeeOption {
388    /// Server-assigned identifier the wallet echoes back to select this option
389    pub fee_index: u32,
390    /// Maximum onchain transaction fee the mint may charge
391    pub fee_reserve: Amount,
392    /// Estimated confirmation target in blocks
393    pub estimated_blocks: u32,
394}
395
396impl From<cdk::nuts::nut30::MeltQuoteOnchainFeeOption> for MeltQuoteOnchainFeeOption {
397    fn from(option: cdk::nuts::nut30::MeltQuoteOnchainFeeOption) -> Self {
398        Self {
399            fee_index: option.fee_index,
400            fee_reserve: option.fee_reserve.into(),
401            estimated_blocks: option.estimated_blocks,
402        }
403    }
404}
405
406/// FFI-compatible MeltQuoteOnchainResponse.
407#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
408pub struct MeltQuoteOnchainResponse {
409    /// Quote ID
410    pub quote: String,
411    /// Amount being paid to the onchain address
412    pub amount: Amount,
413    /// Unit
414    pub unit: CurrencyUnit,
415    /// Quote state
416    pub state: QuoteState,
417    /// Expiry timestamp
418    pub expiry: u64,
419    /// Bitcoin address to pay
420    pub request: String,
421    /// Available onchain fee options
422    pub fee_options: Vec<MeltQuoteOnchainFeeOption>,
423    /// Selected fee option index, once execution has started
424    pub selected_fee_index: Option<u32>,
425    /// Broadcast outpoint (`txid:vout`), once available
426    pub outpoint: Option<String>,
427    /// Change blind signatures as JSON, when the mint returns change
428    pub change: Option<String>,
429}
430
431impl From<cdk::nuts::MeltQuoteOnchainResponse<String>> for MeltQuoteOnchainResponse {
432    fn from(response: cdk::nuts::MeltQuoteOnchainResponse<String>) -> Self {
433        let change = response
434            .change
435            .as_ref()
436            .and_then(|change| serde_json::to_string(change).ok());
437
438        Self {
439            quote: response.quote,
440            amount: response.amount.into(),
441            unit: response.unit.into(),
442            state: response.state.into(),
443            expiry: response.expiry,
444            request: response.request,
445            fee_options: response.fee_options.into_iter().map(Into::into).collect(),
446            selected_fee_index: response.selected_fee_index,
447            outpoint: response.outpoint,
448            change,
449        }
450    }
451}
452
453/// FFI-compatible MeltQuote
454#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
455pub struct MeltQuote {
456    /// Quote ID
457    pub id: String,
458    /// Mint URL
459    pub mint_url: Option<MintUrl>,
460    /// Quote amount
461    pub amount: Amount,
462    /// Currency unit
463    pub unit: CurrencyUnit,
464    /// Payment request
465    pub request: String,
466    /// Fee reserve
467    pub fee_reserve: Amount,
468    /// Quote state
469    pub state: QuoteState,
470    /// Expiry timestamp
471    pub expiry: u64,
472    /// Payment proof (e.g. Lightning preimage or onchain outpoint)
473    pub payment_proof: Option<String>,
474    /// Estimated confirmation target in blocks for onchain quotes
475    pub estimated_blocks: Option<u32>,
476    /// Selected fee option index for onchain quotes
477    pub fee_index: Option<u32>,
478    /// Payment method
479    pub payment_method: PaymentMethod,
480    /// Operation ID that reserved this quote
481    pub used_by_operation: Option<String>,
482    /// Version for optimistic locking
483    #[serde(default)]
484    pub version: u32,
485}
486
487impl From<cdk::wallet::MeltQuote> for MeltQuote {
488    fn from(quote: cdk::wallet::MeltQuote) -> Self {
489        Self {
490            id: quote.id.clone(),
491            mint_url: quote.mint_url.map(Into::into),
492            amount: quote.amount.into(),
493            unit: quote.unit.clone().into(),
494            request: quote.request.clone(),
495            fee_reserve: quote.fee_reserve.into(),
496            state: quote.state.into(),
497            expiry: quote.expiry,
498            payment_proof: quote.payment_proof.clone(),
499            estimated_blocks: quote.estimated_blocks,
500            fee_index: quote.fee_index,
501            payment_method: quote.payment_method.into(),
502            used_by_operation: quote.used_by_operation.map(|id| id.to_string()),
503            version: quote.version,
504        }
505    }
506}
507
508impl TryFrom<MeltQuote> for cdk::wallet::MeltQuote {
509    type Error = FfiError;
510
511    fn try_from(quote: MeltQuote) -> Result<Self, Self::Error> {
512        Ok(Self {
513            id: quote.id,
514            mint_url: quote.mint_url.map(|m| m.try_into()).transpose()?,
515            amount: quote.amount.into(),
516            unit: quote.unit.into(),
517            request: quote.request,
518            fee_reserve: quote.fee_reserve.into(),
519            state: quote.state.into(),
520            expiry: quote.expiry,
521            payment_proof: quote.payment_proof,
522            estimated_blocks: quote.estimated_blocks,
523            fee_index: quote.fee_index,
524            payment_method: quote.payment_method.into(),
525            used_by_operation: quote.used_by_operation,
526            version: quote.version,
527        })
528    }
529}
530
531impl MeltQuote {
532    /// Convert MeltQuote to JSON string
533    pub fn to_json(&self) -> Result<String, FfiError> {
534        Ok(serde_json::to_string(self)?)
535    }
536}
537
538/// Decode MeltQuote from JSON string
539#[uniffi::export]
540pub fn decode_melt_quote(json: String) -> Result<MeltQuote, FfiError> {
541    let quote: cdk::wallet::MeltQuote = serde_json::from_str(&json)?;
542    Ok(quote.into())
543}
544
545/// Encode MeltQuote to JSON string
546#[uniffi::export]
547pub fn encode_melt_quote(quote: MeltQuote) -> Result<String, FfiError> {
548    Ok(serde_json::to_string(&quote)?)
549}
550
551/// FFI-compatible QuoteState
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
553pub enum QuoteState {
554    Unpaid,
555    Paid,
556    Pending,
557    Issued,
558}
559
560impl From<cdk::nuts::nut05::QuoteState> for QuoteState {
561    fn from(state: cdk::nuts::nut05::QuoteState) -> Self {
562        match state {
563            cdk::nuts::nut05::QuoteState::Unpaid => QuoteState::Unpaid,
564            cdk::nuts::nut05::QuoteState::Paid => QuoteState::Paid,
565            cdk::nuts::nut05::QuoteState::Pending => QuoteState::Pending,
566            cdk::nuts::nut05::QuoteState::Unknown => QuoteState::Unpaid,
567            cdk::nuts::nut05::QuoteState::Failed => QuoteState::Unpaid,
568        }
569    }
570}
571
572impl From<QuoteState> for cdk::nuts::nut05::QuoteState {
573    fn from(state: QuoteState) -> Self {
574        match state {
575            QuoteState::Unpaid => cdk::nuts::nut05::QuoteState::Unpaid,
576            QuoteState::Paid => cdk::nuts::nut05::QuoteState::Paid,
577            QuoteState::Pending => cdk::nuts::nut05::QuoteState::Pending,
578            QuoteState::Issued => cdk::nuts::nut05::QuoteState::Paid, // Map issued to paid for melt quotes
579        }
580    }
581}
582
583impl From<cdk::nuts::MintQuoteState> for QuoteState {
584    fn from(state: cdk::nuts::MintQuoteState) -> Self {
585        match state {
586            cdk::nuts::MintQuoteState::Unpaid => QuoteState::Unpaid,
587            cdk::nuts::MintQuoteState::Paid => QuoteState::Paid,
588            cdk::nuts::MintQuoteState::Issued => QuoteState::Issued,
589        }
590    }
591}
592
593impl From<QuoteState> for cdk::nuts::MintQuoteState {
594    fn from(state: QuoteState) -> Self {
595        match state {
596            QuoteState::Unpaid => cdk::nuts::MintQuoteState::Unpaid,
597            QuoteState::Paid => cdk::nuts::MintQuoteState::Paid,
598            QuoteState::Issued => cdk::nuts::MintQuoteState::Issued,
599            QuoteState::Pending => cdk::nuts::MintQuoteState::Unpaid,
600        }
601    }
602}
603
604// Note: MeltQuoteState is the same as nut05::QuoteState, so we don't need a separate impl