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.saga_id.and_then(|id| Uuid::from_str(&id).ok()),
97 })
98 }
99}
100
101impl Transaction {
102 pub fn to_json(&self) -> Result<String, FfiError> {
104 Ok(serde_json::to_string(self)?)
105 }
106}
107
108#[uniffi::export]
110pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
111 Ok(serde_json::from_str(&json)?)
112}
113
114#[uniffi::export]
116pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
117 Ok(serde_json::to_string(&transaction)?)
118}
119
120#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
137pub enum TransactionDirection {
138 Incoming,
140 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
164#[serde(transparent)]
165pub struct TransactionId {
166 pub hex: String,
168}
169
170impl TransactionId {
171 pub fn from_hex(hex: String) -> Result<Self, FfiError> {
173 if hex.len() != 64 {
175 return Err(FfiError::internal(
176 "Transaction ID hex must be exactly 64 characters (32 bytes)",
177 ));
178 }
179
180 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 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
221pub struct AuthProof {
222 pub keyset_id: String,
224 pub secret: String,
226 pub c: String,
228 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, })
263 }
264}
265
266impl AuthProof {
267 pub fn to_json(&self) -> Result<String, FfiError> {
269 Ok(serde_json::to_string(self)?)
270 }
271}
272
273#[uniffi::export]
275pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
276 Ok(serde_json::from_str(&json)?)
277}
278
279#[uniffi::export]
281pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
282 Ok(serde_json::to_string(&proof)?)
283}