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