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.saga_id.and_then(|id| Uuid::from_str(&id).ok()),
97        })
98    }
99}
100
101impl Transaction {
102    /// Convert Transaction to JSON string
103    pub fn to_json(&self) -> Result<String, FfiError> {
104        Ok(serde_json::to_string(self)?)
105    }
106}
107
108/// Decode Transaction from JSON string
109#[uniffi::export]
110pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
111    Ok(serde_json::from_str(&json)?)
112}
113
114/// Encode Transaction to JSON string
115#[uniffi::export]
116pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
117    Ok(serde_json::to_string(&transaction)?)
118}
119
120/// Check if a transaction matches the given filter conditions
121#[uniffi::export]
122pub fn transaction_matches_conditions(
123    transaction: &Transaction,
124    mint_url: Option<MintUrl>,
125    direction: Option<TransactionDirection>,
126    unit: Option<CurrencyUnit>,
127) -> Result<bool, FfiError> {
128    let cdk_transaction: cdk::wallet::types::Transaction = transaction.clone().try_into()?;
129    let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
130    let cdk_direction = direction.map(Into::into);
131    let cdk_unit = unit.map(Into::into);
132    Ok(cdk_transaction.matches_conditions(&cdk_mint_url, &cdk_direction, &cdk_unit))
133}
134
135/// FFI-compatible TransactionDirection
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
137pub enum TransactionDirection {
138    /// Incoming transaction (i.e., receive or mint)
139    Incoming,
140    /// Outgoing transaction (i.e., send or melt)
141    Outgoing,
142}
143
144impl From<cdk::wallet::types::TransactionDirection> for TransactionDirection {
145    fn from(direction: cdk::wallet::types::TransactionDirection) -> Self {
146        match direction {
147            cdk::wallet::types::TransactionDirection::Incoming => TransactionDirection::Incoming,
148            cdk::wallet::types::TransactionDirection::Outgoing => TransactionDirection::Outgoing,
149        }
150    }
151}
152
153impl From<TransactionDirection> for cdk::wallet::types::TransactionDirection {
154    fn from(direction: TransactionDirection) -> Self {
155        match direction {
156            TransactionDirection::Incoming => cdk::wallet::types::TransactionDirection::Incoming,
157            TransactionDirection::Outgoing => cdk::wallet::types::TransactionDirection::Outgoing,
158        }
159    }
160}
161
162/// FFI-compatible TransactionId
163#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
164#[serde(transparent)]
165pub struct TransactionId {
166    /// Hex-encoded transaction ID (64 characters)
167    pub hex: String,
168}
169
170impl TransactionId {
171    /// Create a new TransactionId from hex string
172    pub fn from_hex(hex: String) -> Result<Self, FfiError> {
173        // Validate hex string length (should be 64 characters for 32 bytes)
174        if hex.len() != 64 {
175            return Err(FfiError::internal(
176                "Transaction ID hex must be exactly 64 characters (32 bytes)",
177            ));
178        }
179
180        // Validate hex format
181        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
182            return Err(FfiError::internal(
183                "Transaction ID hex contains invalid characters",
184            ));
185        }
186
187        Ok(Self { hex })
188    }
189
190    /// Create from proofs
191    pub fn from_proofs(proofs: &Proofs) -> Result<Self, FfiError> {
192        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
193            proofs.iter().map(|p| p.clone().try_into()).collect();
194        let cdk_proofs = cdk_proofs?;
195        let id = cdk::wallet::types::TransactionId::from_proofs(cdk_proofs)?;
196        Ok(Self {
197            hex: id.to_string(),
198        })
199    }
200}
201
202impl From<cdk::wallet::types::TransactionId> for TransactionId {
203    fn from(id: cdk::wallet::types::TransactionId) -> Self {
204        Self {
205            hex: id.to_string(),
206        }
207    }
208}
209
210impl TryFrom<TransactionId> for cdk::wallet::types::TransactionId {
211    type Error = FfiError;
212
213    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
214        cdk::wallet::types::TransactionId::from_hex(&id.hex)
215            .map_err(|e| FfiError::internal(format!("Invalid transaction ID: {}", e)))
216    }
217}
218
219/// FFI-compatible AuthProof
220#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
221pub struct AuthProof {
222    /// Keyset ID
223    pub keyset_id: String,
224    /// Secret message
225    pub secret: String,
226    /// Unblinded signature (C)
227    pub c: String,
228    /// Y value (hash_to_curve of secret)
229    pub y: String,
230}
231
232impl From<cdk::nuts::AuthProof> for AuthProof {
233    fn from(auth_proof: cdk::nuts::AuthProof) -> Self {
234        Self {
235            keyset_id: auth_proof.keyset_id.to_string(),
236            secret: auth_proof.secret.to_string(),
237            c: auth_proof.c.to_string(),
238            y: auth_proof
239                .y()
240                .map(|y| y.to_string())
241                .unwrap_or_else(|_| "".to_string()),
242        }
243    }
244}
245
246impl TryFrom<AuthProof> for cdk::nuts::AuthProof {
247    type Error = FfiError;
248
249    fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
250        use std::str::FromStr;
251        Ok(Self {
252            keyset_id: cdk::nuts::Id::from_str(&auth_proof.keyset_id)
253                .map_err(|e| FfiError::internal(format!("Invalid keyset ID: {}", e)))?,
254            secret: {
255                use std::str::FromStr;
256                cdk::secret::Secret::from_str(&auth_proof.secret)
257                    .map_err(|e| FfiError::internal(format!("Invalid secret: {}", e)))?
258            },
259            c: cdk::nuts::PublicKey::from_str(&auth_proof.c)
260                .map_err(|e| FfiError::internal(format!("Invalid public key: {}", e)))?,
261            dleq: None, // FFI doesn't expose DLEQ proofs for simplicity
262        })
263    }
264}
265
266impl AuthProof {
267    /// Convert AuthProof to JSON string
268    pub fn to_json(&self) -> Result<String, FfiError> {
269        Ok(serde_json::to_string(self)?)
270    }
271}
272
273/// Decode AuthProof from JSON string
274#[uniffi::export]
275pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
276    Ok(serde_json::from_str(&json)?)
277}
278
279/// Encode AuthProof to JSON string
280#[uniffi::export]
281pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
282    Ok(serde_json::to_string(&proof)?)
283}