1use 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
18pub struct Transaction {
19 pub id: TransactionId,
21 pub mint_url: MintUrl,
23 pub direction: TransactionDirection,
25 pub amount: Amount,
27 pub fee: Amount,
29 pub unit: CurrencyUnit,
31 pub ys: Vec<PublicKey>,
33 pub timestamp: u64,
35 pub memo: Option<String>,
37 pub metadata: HashMap<String, String>,
39 pub quote_id: Option<String>,
41 pub payment_request: Option<String>,
43 pub payment_proof: Option<String>,
45 pub payment_method: Option<PaymentMethod>,
47 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
73impl 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 pub fn to_json(&self) -> Result<String, FfiError> {
108 Ok(serde_json::to_string(self)?)
109 }
110}
111
112#[uniffi::export]
114pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
115 Ok(serde_json::from_str(&json)?)
116}
117
118#[uniffi::export]
120pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
121 Ok(serde_json::to_string(&transaction)?)
122}
123
124#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
141pub enum TransactionDirection {
142 Incoming,
144 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
168#[serde(transparent)]
169pub struct TransactionId {
170 pub hex: String,
172}
173
174impl TransactionId {
175 pub fn from_hex(hex: String) -> Result<Self, FfiError> {
177 if hex.len() != 64 {
179 return Err(FfiError::internal(
180 "Transaction ID hex must be exactly 64 characters (32 bytes)",
181 ));
182 }
183
184 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 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
225pub struct AuthProof {
226 pub keyset_id: String,
228 pub secret: String,
230 pub c: String,
232 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, })
267 }
268}
269
270impl AuthProof {
271 pub fn to_json(&self) -> Result<String, FfiError> {
273 Ok(serde_json::to_string(self)?)
274 }
275}
276
277#[uniffi::export]
279pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
280 Ok(serde_json::from_str(&json)?)
281}
282
283#[uniffi::export]
285pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
286 Ok(serde_json::to_string(&proof)?)
287}