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