Skip to main content

cdk_ffi/types/
transaction.rs

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