Skip to main content

cdk_ffi/types/
proof.rs

1//! Proof-related FFI types
2
3use std::str::FromStr;
4
5use cdk::nuts::State as CdkState;
6use serde::{Deserialize, Serialize};
7
8use super::amount::{Amount, CurrencyUnit};
9use super::mint::MintUrl;
10use crate::error::FfiError;
11
12/// FFI-compatible Proof state
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
14pub enum ProofState {
15    Unspent,
16    Pending,
17    Spent,
18    Reserved,
19    PendingSpent,
20}
21
22impl From<CdkState> for ProofState {
23    fn from(state: CdkState) -> Self {
24        match state {
25            CdkState::Unspent => ProofState::Unspent,
26            CdkState::Pending => ProofState::Pending,
27            CdkState::Spent => ProofState::Spent,
28            CdkState::Reserved => ProofState::Reserved,
29            CdkState::PendingSpent => ProofState::PendingSpent,
30        }
31    }
32}
33
34impl From<ProofState> for CdkState {
35    fn from(state: ProofState) -> Self {
36        match state {
37            ProofState::Unspent => CdkState::Unspent,
38            ProofState::Pending => CdkState::Pending,
39            ProofState::Spent => CdkState::Spent,
40            ProofState::Reserved => CdkState::Reserved,
41            ProofState::PendingSpent => CdkState::PendingSpent,
42        }
43    }
44}
45
46/// FFI-compatible Proof
47#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
48pub struct Proof {
49    /// Proof amount
50    pub amount: Amount,
51    /// Secret (as string)
52    pub secret: String,
53    /// Unblinded signature C (as hex string)
54    pub c: String,
55    /// Keyset ID (as hex string)
56    pub keyset_id: String,
57    /// Optional witness
58    pub witness: Option<Witness>,
59    /// Optional DLEQ proof
60    pub dleq: Option<ProofDleq>,
61    /// Optional P2BK Ephemeral Public Key (NUT-28)
62    pub p2pk_e: Option<String>,
63}
64
65impl From<cdk::nuts::Proof> for Proof {
66    fn from(proof: cdk::nuts::Proof) -> Self {
67        Self {
68            amount: proof.amount.into(),
69            secret: proof.secret.to_string(),
70            c: proof.c.to_string(),
71            keyset_id: proof.keyset_id.to_string(),
72            witness: proof.witness.map(|w| w.into()),
73            dleq: proof.dleq.map(|d| d.into()),
74            p2pk_e: proof.p2pk_e.map(|p| p.to_string()),
75        }
76    }
77}
78
79impl TryFrom<Proof> for cdk::nuts::Proof {
80    type Error = FfiError;
81
82    fn try_from(proof: Proof) -> Result<Self, Self::Error> {
83        use std::str::FromStr;
84
85        use cdk::nuts::Id;
86
87        Ok(Self {
88            amount: proof.amount.into(),
89            secret: cdk::secret::Secret::from_str(&proof.secret)
90                .map_err(|e| FfiError::internal(format!("Invalid secret: {}", e)))?,
91            c: cdk::nuts::PublicKey::from_str(&proof.c)
92                .map_err(|e| FfiError::internal(format!("Invalid public key: {}", e)))?,
93            keyset_id: Id::from_str(&proof.keyset_id)
94                .map_err(|e| FfiError::internal(format!("Invalid keyset ID: {}", e)))?,
95            witness: proof.witness.map(|w| w.into()),
96            dleq: proof.dleq.map(TryInto::try_into).transpose()?,
97            p2pk_e: proof
98                .p2pk_e
99                .map(|p| cdk::nuts::PublicKey::from_str(&p))
100                .transpose()
101                .map_err(|e| FfiError::internal(format!("Invalid p2pk_e: {}", e)))?,
102        })
103    }
104}
105
106/// Get the Y value (hash_to_curve of secret) for a proof
107#[uniffi::export]
108pub fn proof_y(proof: &Proof) -> Result<String, FfiError> {
109    // Convert to CDK proof to calculate Y
110    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
111    Ok(cdk_proof.y()?.to_string())
112}
113
114/// Check if proof is active with given keyset IDs
115#[uniffi::export]
116pub fn proof_is_active(proof: &Proof, active_keyset_ids: Vec<String>) -> bool {
117    use cdk::nuts::Id;
118    let ids: Vec<Id> = active_keyset_ids
119        .into_iter()
120        .filter_map(|id| Id::from_str(&id).ok())
121        .collect();
122
123    // A proof is active if its keyset_id is in the active list
124    if let Ok(keyset_id) = Id::from_str(&proof.keyset_id) {
125        ids.contains(&keyset_id)
126    } else {
127        false
128    }
129}
130
131/// Check if proof has DLEQ proof
132#[uniffi::export]
133pub fn proof_has_dleq(proof: &Proof) -> bool {
134    proof.dleq.is_some()
135}
136
137/// Verify HTLC witness on a proof
138#[uniffi::export]
139pub fn proof_verify_htlc(proof: &Proof) -> Result<(), FfiError> {
140    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
141    cdk_proof.verify_htlc().map_err(FfiError::internal)
142}
143
144/// Verify DLEQ proof on a proof
145#[uniffi::export]
146pub fn proof_verify_dleq(
147    proof: &Proof,
148    mint_pubkey: super::keys::PublicKey,
149) -> Result<(), FfiError> {
150    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
151    let cdk_pubkey: cdk::nuts::PublicKey = mint_pubkey.try_into()?;
152    cdk_proof
153        .verify_dleq(cdk_pubkey)
154        .map_err(FfiError::internal)
155}
156
157/// Sign a P2PK proof with a secret key, returning a new signed proof
158#[uniffi::export]
159pub fn proof_sign_p2pk(proof: Proof, secret_key_hex: String) -> Result<Proof, FfiError> {
160    let mut cdk_proof: cdk::nuts::Proof = proof.try_into()?;
161    let secret_key = cdk::nuts::SecretKey::from_hex(&secret_key_hex)
162        .map_err(|e| FfiError::internal(format!("Invalid secret key: {}", e)))?;
163
164    cdk_proof
165        .sign_p2pk(secret_key)
166        .map_err(FfiError::internal)?;
167
168    Ok(cdk_proof.into())
169}
170
171/// FFI-compatible Proofs (vector of Proof)
172pub type Proofs = Vec<Proof>;
173
174/// FFI-compatible DLEQ proof for proofs
175#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
176pub struct ProofDleq {
177    /// e value (hex-encoded SecretKey)
178    pub e: String,
179    /// s value (hex-encoded SecretKey)
180    pub s: String,
181    /// r value - blinding factor (hex-encoded SecretKey)
182    pub r: String,
183}
184
185/// FFI-compatible DLEQ proof for blind signatures
186#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
187pub struct BlindSignatureDleq {
188    /// e value (hex-encoded SecretKey)
189    pub e: String,
190    /// s value (hex-encoded SecretKey)
191    pub s: String,
192}
193
194impl From<cdk::nuts::ProofDleq> for ProofDleq {
195    fn from(dleq: cdk::nuts::ProofDleq) -> Self {
196        Self {
197            e: dleq.e.to_secret_hex(),
198            s: dleq.s.to_secret_hex(),
199            r: dleq.r.to_secret_hex(),
200        }
201    }
202}
203
204impl TryFrom<ProofDleq> for cdk::nuts::ProofDleq {
205    type Error = FfiError;
206
207    fn try_from(dleq: ProofDleq) -> Result<Self, Self::Error> {
208        Ok(Self {
209            e: cdk::nuts::SecretKey::from_hex(&dleq.e)
210                .map_err(|e| FfiError::internal(format!("Invalid dleq e: {}", e)))?,
211            s: cdk::nuts::SecretKey::from_hex(&dleq.s)
212                .map_err(|e| FfiError::internal(format!("Invalid dleq s: {}", e)))?,
213            r: cdk::nuts::SecretKey::from_hex(&dleq.r)
214                .map_err(|e| FfiError::internal(format!("Invalid dleq r: {}", e)))?,
215        })
216    }
217}
218
219impl From<cdk::nuts::BlindSignatureDleq> for BlindSignatureDleq {
220    fn from(dleq: cdk::nuts::BlindSignatureDleq) -> Self {
221        Self {
222            e: dleq.e.to_secret_hex(),
223            s: dleq.s.to_secret_hex(),
224        }
225    }
226}
227
228impl TryFrom<BlindSignatureDleq> for cdk::nuts::BlindSignatureDleq {
229    type Error = FfiError;
230
231    fn try_from(dleq: BlindSignatureDleq) -> Result<Self, Self::Error> {
232        Ok(Self {
233            e: cdk::nuts::SecretKey::from_hex(&dleq.e).map_err(|e| {
234                FfiError::internal(format!("Invalid blind signature dleq e: {}", e))
235            })?,
236            s: cdk::nuts::SecretKey::from_hex(&dleq.s).map_err(|e| {
237                FfiError::internal(format!("Invalid blind signature dleq s: {}", e))
238            })?,
239        })
240    }
241}
242
243/// Helper function to calculate total amount of proofs
244#[uniffi::export]
245pub fn proofs_total_amount(proofs: &Proofs) -> Result<Amount, FfiError> {
246    let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
247        proofs.iter().map(|p| p.clone().try_into()).collect();
248    let cdk_proofs = cdk_proofs?;
249    use cdk::nuts::ProofsMethods;
250    Ok(cdk_proofs.total_amount()?.into())
251}
252
253/// FFI-compatible Conditions (for spending conditions)
254#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
255pub struct Conditions {
256    /// Unix locktime after which refund keys can be used
257    pub locktime: Option<u64>,
258    /// Additional Public keys (as hex strings)
259    pub pubkeys: Vec<String>,
260    /// Refund keys (as hex strings)
261    pub refund_keys: Vec<String>,
262    /// Number of signatures required (default 1)
263    pub num_sigs: Option<u64>,
264    /// Signature flag (0 = SigInputs, 1 = SigAll)
265    pub sig_flag: u8,
266    /// Number of refund signatures required (default 1)
267    pub num_sigs_refund: Option<u64>,
268}
269
270impl From<cdk::nuts::nut10::Conditions> for Conditions {
271    fn from(conditions: cdk::nuts::nut10::Conditions) -> Self {
272        Self {
273            locktime: conditions.locktime,
274            pubkeys: conditions
275                .pubkeys
276                .unwrap_or_default()
277                .into_iter()
278                .map(|p| p.to_string())
279                .collect(),
280            refund_keys: conditions
281                .refund_keys
282                .unwrap_or_default()
283                .into_iter()
284                .map(|p| p.to_string())
285                .collect(),
286            num_sigs: conditions.num_sigs,
287            sig_flag: match conditions.sig_flag {
288                cdk::nuts::nut11::SigFlag::SigInputs => 0,
289                cdk::nuts::nut11::SigFlag::SigAll => 1,
290            },
291            num_sigs_refund: conditions.num_sigs_refund,
292        }
293    }
294}
295
296impl TryFrom<Conditions> for cdk::nuts::nut10::Conditions {
297    type Error = FfiError;
298
299    fn try_from(conditions: Conditions) -> Result<Self, Self::Error> {
300        let pubkeys = if conditions.pubkeys.is_empty() {
301            None
302        } else {
303            Some(
304                conditions
305                    .pubkeys
306                    .into_iter()
307                    .map(|s| {
308                        s.parse()
309                            .map_err(|e| FfiError::internal(format!("Invalid pubkey: {}", e)))
310                    })
311                    .collect::<Result<Vec<_>, _>>()?,
312            )
313        };
314
315        let refund_keys = if conditions.refund_keys.is_empty() {
316            None
317        } else {
318            Some(
319                conditions
320                    .refund_keys
321                    .into_iter()
322                    .map(|s| {
323                        s.parse()
324                            .map_err(|e| FfiError::internal(format!("Invalid refund key: {}", e)))
325                    })
326                    .collect::<Result<Vec<_>, _>>()?,
327            )
328        };
329
330        let sig_flag = match conditions.sig_flag {
331            0 => cdk::nuts::nut11::SigFlag::SigInputs,
332            1 => cdk::nuts::nut11::SigFlag::SigAll,
333            _ => return Err(FfiError::internal("Invalid sig_flag value")),
334        };
335
336        Ok(Self {
337            locktime: conditions.locktime,
338            pubkeys,
339            refund_keys,
340            num_sigs: conditions.num_sigs,
341            sig_flag,
342            num_sigs_refund: conditions.num_sigs_refund,
343        })
344    }
345}
346
347impl Conditions {
348    /// Convert Conditions to JSON string
349    pub fn to_json(&self) -> Result<String, FfiError> {
350        Ok(serde_json::to_string(self)?)
351    }
352}
353
354/// Decode Conditions from JSON string
355#[uniffi::export]
356pub fn decode_conditions(json: String) -> Result<Conditions, FfiError> {
357    Ok(serde_json::from_str(&json)?)
358}
359
360/// Encode Conditions to JSON string
361#[uniffi::export]
362pub fn encode_conditions(conditions: Conditions) -> Result<String, FfiError> {
363    Ok(serde_json::to_string(&conditions)?)
364}
365
366/// FFI-compatible Witness
367#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
368pub enum Witness {
369    /// P2PK Witness
370    P2PK {
371        /// Signatures
372        signatures: Vec<String>,
373    },
374    /// HTLC Witness
375    HTLC {
376        /// Preimage
377        preimage: String,
378        /// Optional signatures
379        signatures: Option<Vec<String>>,
380    },
381}
382
383impl From<cdk::nuts::Witness> for Witness {
384    fn from(witness: cdk::nuts::Witness) -> Self {
385        match witness {
386            cdk::nuts::Witness::P2PKWitness(p2pk) => Self::P2PK {
387                signatures: p2pk.signatures,
388            },
389            cdk::nuts::Witness::HTLCWitness(htlc) => Self::HTLC {
390                preimage: htlc.preimage,
391                signatures: htlc.signatures,
392            },
393        }
394    }
395}
396
397impl From<Witness> for cdk::nuts::Witness {
398    fn from(witness: Witness) -> Self {
399        match witness {
400            Witness::P2PK { signatures } => {
401                Self::P2PKWitness(cdk::nuts::nut11::P2PKWitness { signatures })
402            }
403            Witness::HTLC {
404                preimage,
405                signatures,
406            } => Self::HTLCWitness(cdk::nuts::nut14::HTLCWitness {
407                preimage,
408                signatures,
409            }),
410        }
411    }
412}
413
414/// FFI-compatible SpendingConditions
415#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
416pub enum SpendingConditions {
417    /// P2PK (Pay to Public Key) conditions
418    P2PK {
419        /// The public key (as hex string)
420        pubkey: String,
421        /// Additional conditions
422        conditions: Option<Conditions>,
423    },
424    /// HTLC (Hash Time Locked Contract) conditions
425    HTLC {
426        /// Hash of the preimage (as hex string)
427        hash: String,
428        /// Additional conditions
429        conditions: Option<Conditions>,
430    },
431}
432
433impl From<cdk::nuts::SpendingConditions> for SpendingConditions {
434    fn from(spending_conditions: cdk::nuts::SpendingConditions) -> Self {
435        match spending_conditions {
436            cdk::nuts::SpendingConditions::P2PKConditions { data, conditions } => Self::P2PK {
437                pubkey: data.to_string(),
438                conditions: conditions.map(Into::into),
439            },
440            cdk::nuts::SpendingConditions::HTLCConditions { data, conditions } => Self::HTLC {
441                hash: data.to_string(),
442                conditions: conditions.map(Into::into),
443            },
444        }
445    }
446}
447
448impl TryFrom<SpendingConditions> for cdk::nuts::SpendingConditions {
449    type Error = FfiError;
450
451    fn try_from(spending_conditions: SpendingConditions) -> Result<Self, Self::Error> {
452        match spending_conditions {
453            SpendingConditions::P2PK { pubkey, conditions } => {
454                let pubkey = pubkey
455                    .parse()
456                    .map_err(|e| FfiError::internal(format!("Invalid pubkey: {}", e)))?;
457                let conditions = conditions.map(|c| c.try_into()).transpose()?;
458                Ok(Self::P2PKConditions {
459                    data: pubkey,
460                    conditions,
461                })
462            }
463            SpendingConditions::HTLC { hash, conditions } => {
464                let hash = hash
465                    .parse()
466                    .map_err(|e| FfiError::internal(format!("Invalid hash: {}", e)))?;
467                let conditions = conditions.map(|c| c.try_into()).transpose()?;
468                Ok(Self::HTLCConditions {
469                    data: hash,
470                    conditions,
471                })
472            }
473        }
474    }
475}
476
477/// FFI-compatible ProofInfo
478#[derive(Debug, Clone, uniffi::Record)]
479pub struct ProofInfo {
480    /// Proof
481    pub proof: Proof,
482    /// Y value (hash_to_curve of secret)
483    pub y: super::keys::PublicKey,
484    /// Mint URL
485    pub mint_url: MintUrl,
486    /// Proof state
487    pub state: ProofState,
488    /// Proof Spending Conditions
489    pub spending_condition: Option<SpendingConditions>,
490    /// Currency unit
491    pub unit: CurrencyUnit,
492    /// Operation ID that is using/spending this proof
493    pub used_by_operation: Option<String>,
494    /// Operation ID that created this proof
495    pub created_by_operation: Option<String>,
496}
497
498impl From<cdk::types::ProofInfo> for ProofInfo {
499    fn from(info: cdk::types::ProofInfo) -> Self {
500        Self {
501            proof: info.proof.into(),
502            y: info.y.into(),
503            mint_url: info.mint_url.into(),
504            state: info.state.into(),
505            spending_condition: info.spending_condition.map(Into::into),
506            unit: info.unit.into(),
507            used_by_operation: info.used_by_operation.map(|u| u.to_string()),
508            created_by_operation: info.created_by_operation.map(|u| u.to_string()),
509        }
510    }
511}
512
513/// Decode ProofInfo from JSON string
514#[uniffi::export]
515pub fn decode_proof_info(json: String) -> Result<ProofInfo, FfiError> {
516    let info: cdk::types::ProofInfo = serde_json::from_str(&json)?;
517    Ok(info.into())
518}
519
520/// Encode ProofInfo to JSON string
521#[uniffi::export]
522pub fn encode_proof_info(info: ProofInfo) -> Result<String, FfiError> {
523    use std::str::FromStr;
524    // Convert to cdk::types::ProofInfo for serialization
525    let cdk_info = cdk::types::ProofInfo {
526        proof: info.proof.try_into()?,
527        y: info.y.try_into()?,
528        mint_url: info.mint_url.try_into()?,
529        state: info.state.into(),
530        spending_condition: info.spending_condition.map(TryInto::try_into).transpose()?,
531        unit: info.unit.into(),
532        used_by_operation: info
533            .used_by_operation
534            .map(|id| uuid::Uuid::from_str(&id))
535            .transpose()
536            .map_err(|e| FfiError::internal(e.to_string()))?,
537        created_by_operation: info
538            .created_by_operation
539            .map(|id| uuid::Uuid::from_str(&id))
540            .transpose()
541            .map_err(|e| FfiError::internal(e.to_string()))?,
542    };
543    Ok(serde_json::to_string(&cdk_info)?)
544}
545
546/// FFI-compatible ProofStateUpdate
547#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
548pub struct ProofStateUpdate {
549    /// Y value (hash_to_curve of secret)
550    pub y: String,
551    /// Current state
552    pub state: ProofState,
553    /// Optional witness data
554    pub witness: Option<String>,
555}
556
557impl From<cdk::nuts::nut07::ProofState> for ProofStateUpdate {
558    fn from(proof_state: cdk::nuts::nut07::ProofState) -> Self {
559        Self {
560            y: proof_state.y.to_string(),
561            state: proof_state.state.into(),
562            witness: proof_state.witness.map(|w| format!("{:?}", w)),
563        }
564    }
565}
566
567impl ProofStateUpdate {
568    /// Convert ProofStateUpdate to JSON string
569    pub fn to_json(&self) -> Result<String, FfiError> {
570        Ok(serde_json::to_string(self)?)
571    }
572}
573
574/// Decode ProofStateUpdate from JSON string
575#[uniffi::export]
576pub fn decode_proof_state_update(json: String) -> Result<ProofStateUpdate, FfiError> {
577    Ok(serde_json::from_str(&json)?)
578}
579
580/// Encode ProofStateUpdate to JSON string
581#[uniffi::export]
582pub fn encode_proof_state_update(update: ProofStateUpdate) -> Result<String, FfiError> {
583    Ok(serde_json::to_string(&update)?)
584}