cdk_ffi/types/
transaction.rs

1//! Transaction-related FFI types
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::amount::{Amount, CurrencyUnit};
8use super::keys::PublicKey;
9use super::mint::MintUrl;
10use super::proof::Proofs;
11use crate::error::FfiError;
12
13/// FFI-compatible Transaction
14#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
15pub struct Transaction {
16    /// Transaction ID
17    pub id: TransactionId,
18    /// Mint URL
19    pub mint_url: MintUrl,
20    /// Transaction direction
21    pub direction: TransactionDirection,
22    /// Amount
23    pub amount: Amount,
24    /// Fee
25    pub fee: Amount,
26    /// Currency Unit
27    pub unit: CurrencyUnit,
28    /// Proof Ys (Y values from proofs)
29    pub ys: Vec<PublicKey>,
30    /// Unix timestamp
31    pub timestamp: u64,
32    /// Memo
33    pub memo: Option<String>,
34    /// User-defined metadata
35    pub metadata: HashMap<String, String>,
36    /// Quote ID if this is a mint or melt transaction
37    pub quote_id: Option<String>,
38    /// Payment request (e.g., BOLT11 invoice, BOLT12 offer)
39    pub payment_request: Option<String>,
40    /// Payment proof (e.g., preimage for Lightning melt transactions)
41    pub payment_proof: Option<String>,
42}
43
44impl From<cdk::wallet::types::Transaction> for Transaction {
45    fn from(tx: cdk::wallet::types::Transaction) -> Self {
46        Self {
47            id: tx.id().into(),
48            mint_url: tx.mint_url.into(),
49            direction: tx.direction.into(),
50            amount: tx.amount.into(),
51            fee: tx.fee.into(),
52            unit: tx.unit.into(),
53            ys: tx.ys.into_iter().map(Into::into).collect(),
54            timestamp: tx.timestamp,
55            memo: tx.memo,
56            metadata: tx.metadata,
57            quote_id: tx.quote_id,
58            payment_request: tx.payment_request,
59            payment_proof: tx.payment_proof,
60        }
61    }
62}
63
64/// Convert FFI Transaction to CDK Transaction
65impl TryFrom<Transaction> for cdk::wallet::types::Transaction {
66    type Error = FfiError;
67
68    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
69        let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, _> =
70            tx.ys.into_iter().map(|pk| pk.try_into()).collect();
71        let cdk_ys = cdk_ys?;
72
73        Ok(Self {
74            mint_url: tx.mint_url.try_into()?,
75            direction: tx.direction.into(),
76            amount: tx.amount.into(),
77            fee: tx.fee.into(),
78            unit: tx.unit.into(),
79            ys: cdk_ys,
80            timestamp: tx.timestamp,
81            memo: tx.memo,
82            metadata: tx.metadata,
83            quote_id: tx.quote_id,
84            payment_request: tx.payment_request,
85            payment_proof: tx.payment_proof,
86        })
87    }
88}
89
90impl Transaction {
91    /// Convert Transaction to JSON string
92    pub fn to_json(&self) -> Result<String, FfiError> {
93        Ok(serde_json::to_string(self)?)
94    }
95}
96
97/// Decode Transaction from JSON string
98#[uniffi::export]
99pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
100    Ok(serde_json::from_str(&json)?)
101}
102
103/// Encode Transaction to JSON string
104#[uniffi::export]
105pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
106    Ok(serde_json::to_string(&transaction)?)
107}
108
109/// Check if a transaction matches the given filter conditions
110#[uniffi::export]
111pub fn transaction_matches_conditions(
112    transaction: &Transaction,
113    mint_url: Option<MintUrl>,
114    direction: Option<TransactionDirection>,
115    unit: Option<CurrencyUnit>,
116) -> Result<bool, FfiError> {
117    let cdk_transaction: cdk::wallet::types::Transaction = transaction.clone().try_into()?;
118    let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
119    let cdk_direction = direction.map(Into::into);
120    let cdk_unit = unit.map(Into::into);
121    Ok(cdk_transaction.matches_conditions(&cdk_mint_url, &cdk_direction, &cdk_unit))
122}
123
124/// FFI-compatible TransactionDirection
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
126pub enum TransactionDirection {
127    /// Incoming transaction (i.e., receive or mint)
128    Incoming,
129    /// Outgoing transaction (i.e., send or melt)
130    Outgoing,
131}
132
133impl From<cdk::wallet::types::TransactionDirection> for TransactionDirection {
134    fn from(direction: cdk::wallet::types::TransactionDirection) -> Self {
135        match direction {
136            cdk::wallet::types::TransactionDirection::Incoming => TransactionDirection::Incoming,
137            cdk::wallet::types::TransactionDirection::Outgoing => TransactionDirection::Outgoing,
138        }
139    }
140}
141
142impl From<TransactionDirection> for cdk::wallet::types::TransactionDirection {
143    fn from(direction: TransactionDirection) -> Self {
144        match direction {
145            TransactionDirection::Incoming => cdk::wallet::types::TransactionDirection::Incoming,
146            TransactionDirection::Outgoing => cdk::wallet::types::TransactionDirection::Outgoing,
147        }
148    }
149}
150
151/// FFI-compatible TransactionId
152#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
153#[serde(transparent)]
154pub struct TransactionId {
155    /// Hex-encoded transaction ID (64 characters)
156    pub hex: String,
157}
158
159impl TransactionId {
160    /// Create a new TransactionId from hex string
161    pub fn from_hex(hex: String) -> Result<Self, FfiError> {
162        // Validate hex string length (should be 64 characters for 32 bytes)
163        if hex.len() != 64 {
164            return Err(FfiError::InvalidHex {
165                msg: "Transaction ID hex must be exactly 64 characters (32 bytes)".to_string(),
166            });
167        }
168
169        // Validate hex format
170        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
171            return Err(FfiError::InvalidHex {
172                msg: "Transaction ID hex contains invalid characters".to_string(),
173            });
174        }
175
176        Ok(Self { hex })
177    }
178
179    /// Create from proofs
180    pub fn from_proofs(proofs: &Proofs) -> Result<Self, FfiError> {
181        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
182            proofs.iter().map(|p| p.clone().try_into()).collect();
183        let cdk_proofs = cdk_proofs?;
184        let id = cdk::wallet::types::TransactionId::from_proofs(cdk_proofs)?;
185        Ok(Self {
186            hex: id.to_string(),
187        })
188    }
189}
190
191impl From<cdk::wallet::types::TransactionId> for TransactionId {
192    fn from(id: cdk::wallet::types::TransactionId) -> Self {
193        Self {
194            hex: id.to_string(),
195        }
196    }
197}
198
199impl TryFrom<TransactionId> for cdk::wallet::types::TransactionId {
200    type Error = FfiError;
201
202    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
203        cdk::wallet::types::TransactionId::from_hex(&id.hex)
204            .map_err(|e| FfiError::InvalidHex { msg: e.to_string() })
205    }
206}
207
208/// FFI-compatible AuthProof
209#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
210pub struct AuthProof {
211    /// Keyset ID
212    pub keyset_id: String,
213    /// Secret message
214    pub secret: String,
215    /// Unblinded signature (C)
216    pub c: String,
217    /// Y value (hash_to_curve of secret)
218    pub y: String,
219}
220
221impl From<cdk::nuts::AuthProof> for AuthProof {
222    fn from(auth_proof: cdk::nuts::AuthProof) -> Self {
223        Self {
224            keyset_id: auth_proof.keyset_id.to_string(),
225            secret: auth_proof.secret.to_string(),
226            c: auth_proof.c.to_string(),
227            y: auth_proof
228                .y()
229                .map(|y| y.to_string())
230                .unwrap_or_else(|_| "".to_string()),
231        }
232    }
233}
234
235impl TryFrom<AuthProof> for cdk::nuts::AuthProof {
236    type Error = FfiError;
237
238    fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
239        use std::str::FromStr;
240        Ok(Self {
241            keyset_id: cdk::nuts::Id::from_str(&auth_proof.keyset_id)
242                .map_err(|e| FfiError::Serialization { msg: e.to_string() })?,
243            secret: {
244                use std::str::FromStr;
245                cdk::secret::Secret::from_str(&auth_proof.secret)
246                    .map_err(|e| FfiError::Serialization { msg: e.to_string() })?
247            },
248            c: cdk::nuts::PublicKey::from_str(&auth_proof.c)
249                .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?,
250            dleq: None, // FFI doesn't expose DLEQ proofs for simplicity
251        })
252    }
253}
254
255impl AuthProof {
256    /// Convert AuthProof to JSON string
257    pub fn to_json(&self) -> Result<String, FfiError> {
258        Ok(serde_json::to_string(self)?)
259    }
260}
261
262/// Decode AuthProof from JSON string
263#[uniffi::export]
264pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
265    Ok(serde_json::from_str(&json)?)
266}
267
268/// Encode AuthProof to JSON string
269#[uniffi::export]
270pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
271    Ok(serde_json::to_string(&proof)?)
272}