//! FFI-compatible types use std::collections::HashMap; use std::str::FromStr; use std::sync::Mutex; use cdk::nuts::{CurrencyUnit as CdkCurrencyUnit, State as CdkState}; use cdk::pub_sub::SubId; use cdk::Amount as CdkAmount; use serde::{Deserialize, Serialize}; use crate::error::FfiError; /// FFI-compatible Amount type #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct Amount { pub value: u64, } impl Amount { pub fn new(value: u64) -> Self { Self { value } } pub fn zero() -> Self { Self { value: 0 } } pub fn is_zero(&self) -> bool { self.value == 0 } pub fn convert_unit( &self, current_unit: CurrencyUnit, target_unit: CurrencyUnit, ) -> Result { Ok(CdkAmount::from(self.value) .convert_unit(¤t_unit.into(), &target_unit.into()) .map(Into::into)?) } pub fn add(&self, other: Amount) -> Result { let self_amount = CdkAmount::from(self.value); let other_amount = CdkAmount::from(other.value); self_amount .checked_add(other_amount) .map(Into::into) .ok_or(FfiError::AmountOverflow) } pub fn subtract(&self, other: Amount) -> Result { let self_amount = CdkAmount::from(self.value); let other_amount = CdkAmount::from(other.value); self_amount .checked_sub(other_amount) .map(Into::into) .ok_or(FfiError::AmountOverflow) } pub fn multiply(&self, factor: u64) -> Result { let self_amount = CdkAmount::from(self.value); let factor_amount = CdkAmount::from(factor); self_amount .checked_mul(factor_amount) .map(Into::into) .ok_or(FfiError::AmountOverflow) } pub fn divide(&self, divisor: u64) -> Result { if divisor == 0 { return Err(FfiError::DivisionByZero); } let self_amount = CdkAmount::from(self.value); let divisor_amount = CdkAmount::from(divisor); self_amount .checked_div(divisor_amount) .map(Into::into) .ok_or(FfiError::AmountOverflow) } } impl From for Amount { fn from(amount: CdkAmount) -> Self { Self { value: u64::from(amount), } } } impl From for CdkAmount { fn from(amount: Amount) -> Self { CdkAmount::from(amount.value) } } /// FFI-compatible Currency Unit #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum CurrencyUnit { Sat, Msat, Usd, Eur, Auth, Custom { unit: String }, } impl From for CurrencyUnit { fn from(unit: CdkCurrencyUnit) -> Self { match unit { CdkCurrencyUnit::Sat => CurrencyUnit::Sat, CdkCurrencyUnit::Msat => CurrencyUnit::Msat, CdkCurrencyUnit::Usd => CurrencyUnit::Usd, CdkCurrencyUnit::Eur => CurrencyUnit::Eur, CdkCurrencyUnit::Auth => CurrencyUnit::Auth, CdkCurrencyUnit::Custom(s) => CurrencyUnit::Custom { unit: s }, _ => CurrencyUnit::Sat, // Default for unknown units } } } impl From for CdkCurrencyUnit { fn from(unit: CurrencyUnit) -> Self { match unit { CurrencyUnit::Sat => CdkCurrencyUnit::Sat, CurrencyUnit::Msat => CdkCurrencyUnit::Msat, CurrencyUnit::Usd => CdkCurrencyUnit::Usd, CurrencyUnit::Eur => CdkCurrencyUnit::Eur, CurrencyUnit::Auth => CdkCurrencyUnit::Auth, CurrencyUnit::Custom { unit } => CdkCurrencyUnit::Custom(unit), } } } /// FFI-compatible Mint URL #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct MintUrl { pub url: String, } impl MintUrl { pub fn new(url: String) -> Result { // Validate URL format url::Url::parse(&url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?; Ok(Self { url }) } } impl From for MintUrl { fn from(mint_url: cdk::mint_url::MintUrl) -> Self { Self { url: mint_url.to_string(), } } } impl TryFrom for cdk::mint_url::MintUrl { type Error = FfiError; fn try_from(mint_url: MintUrl) -> Result { cdk::mint_url::MintUrl::from_str(&mint_url.url) .map_err(|e| FfiError::InvalidUrl { msg: e.to_string() }) } } /// FFI-compatible Proof state #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum ProofState { Unspent, Pending, Spent, Reserved, PendingSpent, } impl From for ProofState { fn from(state: CdkState) -> Self { match state { CdkState::Unspent => ProofState::Unspent, CdkState::Pending => ProofState::Pending, CdkState::Spent => ProofState::Spent, CdkState::Reserved => ProofState::Reserved, CdkState::PendingSpent => ProofState::PendingSpent, } } } impl From for CdkState { fn from(state: ProofState) -> Self { match state { ProofState::Unspent => CdkState::Unspent, ProofState::Pending => CdkState::Pending, ProofState::Spent => CdkState::Spent, ProofState::Reserved => CdkState::Reserved, ProofState::PendingSpent => CdkState::PendingSpent, } } } /// FFI-compatible Token #[derive(Debug, uniffi::Object)] pub struct Token { pub(crate) inner: cdk::nuts::Token, } impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.inner) } } impl FromStr for Token { type Err = FfiError; fn from_str(s: &str) -> Result { let token = cdk::nuts::Token::from_str(s) .map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?; Ok(Token { inner: token }) } } impl From for Token { fn from(token: cdk::nuts::Token) -> Self { Self { inner: token } } } impl From for cdk::nuts::Token { fn from(token: Token) -> Self { token.inner } } #[uniffi::export] impl Token { /// Create a new Token from string #[uniffi::constructor] pub fn from_string(encoded_token: String) -> Result { let token = cdk::nuts::Token::from_str(&encoded_token) .map_err(|e| FfiError::InvalidToken { msg: e.to_string() })?; Ok(Token { inner: token }) } /// Get the total value of the token pub fn value(&self) -> Result { Ok(self.inner.value()?.into()) } /// Get the memo from the token pub fn memo(&self) -> Option { self.inner.memo().clone() } /// Get the currency unit pub fn unit(&self) -> Option { self.inner.unit().map(Into::into) } /// Get the mint URL pub fn mint_url(&self) -> Result { Ok(self.inner.mint_url()?.into()) } /// Get proofs from the token (simplified - no keyset filtering for now) pub fn proofs_simple(&self) -> Result { // For now, return empty keysets to get all proofs let empty_keysets = vec![]; let proofs = self.inner.proofs(&empty_keysets)?; Ok(proofs .into_iter() .map(|p| std::sync::Arc::new(p.into())) .collect()) } /// Convert token to raw bytes pub fn to_raw_bytes(&self) -> Result, FfiError> { Ok(self.inner.to_raw_bytes()?) } /// Encode token to string representation pub fn encode(&self) -> String { self.to_string() } /// Decode token from string representation #[uniffi::constructor] pub fn decode(encoded_token: String) -> Result { encoded_token.parse() } } /// FFI-compatible SendMemo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct SendMemo { /// Memo text pub memo: String, /// Include memo in token pub include_memo: bool, } impl From for cdk::wallet::SendMemo { fn from(memo: SendMemo) -> Self { cdk::wallet::SendMemo { memo: memo.memo, include_memo: memo.include_memo, } } } impl From for SendMemo { fn from(memo: cdk::wallet::SendMemo) -> Self { Self { memo: memo.memo, include_memo: memo.include_memo, } } } impl SendMemo { /// Convert SendMemo to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode SendMemo from JSON string #[uniffi::export] pub fn decode_send_memo(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode SendMemo to JSON string #[uniffi::export] pub fn encode_send_memo(memo: SendMemo) -> Result { Ok(serde_json::to_string(&memo)?) } /// FFI-compatible SplitTarget #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] pub enum SplitTarget { /// Default target; least amount of proofs None, /// Target amount for wallet to have most proofs that add up to value Value { amount: Amount }, /// Specific amounts to split into (must equal amount being split) Values { amounts: Vec }, } impl From for cdk::amount::SplitTarget { fn from(target: SplitTarget) -> Self { match target { SplitTarget::None => cdk::amount::SplitTarget::None, SplitTarget::Value { amount } => cdk::amount::SplitTarget::Value(amount.into()), SplitTarget::Values { amounts } => { cdk::amount::SplitTarget::Values(amounts.into_iter().map(Into::into).collect()) } } } } impl From for SplitTarget { fn from(target: cdk::amount::SplitTarget) -> Self { match target { cdk::amount::SplitTarget::None => SplitTarget::None, cdk::amount::SplitTarget::Value(amount) => SplitTarget::Value { amount: amount.into(), }, cdk::amount::SplitTarget::Values(amounts) => SplitTarget::Values { amounts: amounts.into_iter().map(Into::into).collect(), }, } } } /// FFI-compatible SendKind #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] pub enum SendKind { /// Allow online swap before send if wallet does not have exact amount OnlineExact, /// Prefer offline send if difference is less than tolerance OnlineTolerance { tolerance: Amount }, /// Wallet cannot do an online swap and selected proof must be exactly send amount OfflineExact, /// Wallet must remain offline but can over pay if below tolerance OfflineTolerance { tolerance: Amount }, } impl From for cdk::wallet::SendKind { fn from(kind: SendKind) -> Self { match kind { SendKind::OnlineExact => cdk::wallet::SendKind::OnlineExact, SendKind::OnlineTolerance { tolerance } => { cdk::wallet::SendKind::OnlineTolerance(tolerance.into()) } SendKind::OfflineExact => cdk::wallet::SendKind::OfflineExact, SendKind::OfflineTolerance { tolerance } => { cdk::wallet::SendKind::OfflineTolerance(tolerance.into()) } } } } impl From for SendKind { fn from(kind: cdk::wallet::SendKind) -> Self { match kind { cdk::wallet::SendKind::OnlineExact => SendKind::OnlineExact, cdk::wallet::SendKind::OnlineTolerance(tolerance) => SendKind::OnlineTolerance { tolerance: tolerance.into(), }, cdk::wallet::SendKind::OfflineExact => SendKind::OfflineExact, cdk::wallet::SendKind::OfflineTolerance(tolerance) => SendKind::OfflineTolerance { tolerance: tolerance.into(), }, } } } /// FFI-compatible Send options #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct SendOptions { /// Memo pub memo: Option, /// Spending conditions pub conditions: Option, /// Amount split target pub amount_split_target: SplitTarget, /// Send kind pub send_kind: SendKind, /// Include fee pub include_fee: bool, /// Maximum number of proofs to include in the token pub max_proofs: Option, /// Metadata pub metadata: HashMap, } impl Default for SendOptions { fn default() -> Self { Self { memo: None, conditions: None, amount_split_target: SplitTarget::None, send_kind: SendKind::OnlineExact, include_fee: false, max_proofs: None, metadata: HashMap::new(), } } } impl From for cdk::wallet::SendOptions { fn from(opts: SendOptions) -> Self { cdk::wallet::SendOptions { memo: opts.memo.map(Into::into), conditions: opts.conditions.and_then(|c| c.try_into().ok()), amount_split_target: opts.amount_split_target.into(), send_kind: opts.send_kind.into(), include_fee: opts.include_fee, max_proofs: opts.max_proofs.map(|p| p as usize), metadata: opts.metadata, } } } impl From for SendOptions { fn from(opts: cdk::wallet::SendOptions) -> Self { Self { memo: opts.memo.map(Into::into), conditions: opts.conditions.map(Into::into), amount_split_target: opts.amount_split_target.into(), send_kind: opts.send_kind.into(), include_fee: opts.include_fee, max_proofs: opts.max_proofs.map(|p| p as u32), metadata: opts.metadata, } } } impl SendOptions { /// Convert SendOptions to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode SendOptions from JSON string #[uniffi::export] pub fn decode_send_options(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode SendOptions to JSON string #[uniffi::export] pub fn encode_send_options(options: SendOptions) -> Result { Ok(serde_json::to_string(&options)?) } /// FFI-compatible SecretKey #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct SecretKey { /// Hex-encoded secret key (64 characters) pub hex: String, } impl SecretKey { /// Create a new SecretKey from hex string pub fn from_hex(hex: String) -> Result { // Validate hex string length (should be 64 characters for 32 bytes) if hex.len() != 64 { return Err(FfiError::InvalidHex { msg: "Secret key hex must be exactly 64 characters (32 bytes)".to_string(), }); } // Validate hex format if !hex.chars().all(|c| c.is_ascii_hexdigit()) { return Err(FfiError::InvalidHex { msg: "Secret key hex contains invalid characters".to_string(), }); } Ok(Self { hex }) } /// Generate a random secret key pub fn random() -> Self { use cdk::nuts::SecretKey as CdkSecretKey; let secret_key = CdkSecretKey::generate(); Self { hex: secret_key.to_secret_hex(), } } } impl From for cdk::nuts::SecretKey { fn from(key: SecretKey) -> Self { // This will panic if hex is invalid, but we validate in from_hex() cdk::nuts::SecretKey::from_hex(&key.hex).expect("Invalid secret key hex") } } impl From for SecretKey { fn from(key: cdk::nuts::SecretKey) -> Self { Self { hex: key.to_secret_hex(), } } } /// FFI-compatible Receive options #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ReceiveOptions { /// Amount split target pub amount_split_target: SplitTarget, /// P2PK signing keys pub p2pk_signing_keys: Vec, /// Preimages for HTLC conditions pub preimages: Vec, /// Metadata pub metadata: HashMap, } impl Default for ReceiveOptions { fn default() -> Self { Self { amount_split_target: SplitTarget::None, p2pk_signing_keys: Vec::new(), preimages: Vec::new(), metadata: HashMap::new(), } } } impl From for cdk::wallet::ReceiveOptions { fn from(opts: ReceiveOptions) -> Self { cdk::wallet::ReceiveOptions { amount_split_target: opts.amount_split_target.into(), p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(), preimages: opts.preimages, metadata: opts.metadata, } } } impl From for ReceiveOptions { fn from(opts: cdk::wallet::ReceiveOptions) -> Self { Self { amount_split_target: opts.amount_split_target.into(), p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(), preimages: opts.preimages, metadata: opts.metadata, } } } impl ReceiveOptions { /// Convert ReceiveOptions to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode ReceiveOptions from JSON string #[uniffi::export] pub fn decode_receive_options(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode ReceiveOptions to JSON string #[uniffi::export] pub fn encode_receive_options(options: ReceiveOptions) -> Result { Ok(serde_json::to_string(&options)?) } /// FFI-compatible Proof #[derive(Debug, uniffi::Object)] pub struct Proof { pub(crate) inner: cdk::nuts::Proof, } impl From for Proof { fn from(proof: cdk::nuts::Proof) -> Self { Self { inner: proof } } } impl From for cdk::nuts::Proof { fn from(proof: Proof) -> Self { proof.inner } } #[uniffi::export] impl Proof { /// Get the amount pub fn amount(&self) -> Amount { self.inner.amount.into() } /// Get the secret as string pub fn secret(&self) -> String { self.inner.secret.to_string() } /// Get the unblinded signature (C) as string pub fn c(&self) -> String { self.inner.c.to_string() } /// Get the keyset ID as string pub fn keyset_id(&self) -> String { self.inner.keyset_id.to_string() } /// Get the witness pub fn witness(&self) -> Option { self.inner.witness.as_ref().map(|w| w.clone().into()) } /// Check if proof is active with given keyset IDs pub fn is_active(&self, active_keyset_ids: Vec) -> bool { use cdk::nuts::Id; let ids: Vec = active_keyset_ids .into_iter() .filter_map(|id| Id::from_str(&id).ok()) .collect(); self.inner.is_active(&ids) } /// Get the Y value (hash_to_curve of secret) pub fn y(&self) -> Result { Ok(self.inner.y()?.to_string()) } /// Get the DLEQ proof if present pub fn dleq(&self) -> Option { self.inner.dleq.as_ref().map(|d| d.clone().into()) } /// Check if proof has DLEQ proof pub fn has_dleq(&self) -> bool { self.inner.dleq.is_some() } } /// FFI-compatible Proofs (vector of Proof) pub type Proofs = Vec>; /// FFI-compatible DLEQ proof for proofs #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ProofDleq { /// e value (hex-encoded SecretKey) pub e: String, /// s value (hex-encoded SecretKey) pub s: String, /// r value - blinding factor (hex-encoded SecretKey) pub r: String, } /// FFI-compatible DLEQ proof for blind signatures #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct BlindSignatureDleq { /// e value (hex-encoded SecretKey) pub e: String, /// s value (hex-encoded SecretKey) pub s: String, } impl From for ProofDleq { fn from(dleq: cdk::nuts::ProofDleq) -> Self { Self { e: dleq.e.to_secret_hex(), s: dleq.s.to_secret_hex(), r: dleq.r.to_secret_hex(), } } } impl From for cdk::nuts::ProofDleq { fn from(dleq: ProofDleq) -> Self { Self { e: cdk::nuts::SecretKey::from_hex(&dleq.e).expect("Invalid e hex"), s: cdk::nuts::SecretKey::from_hex(&dleq.s).expect("Invalid s hex"), r: cdk::nuts::SecretKey::from_hex(&dleq.r).expect("Invalid r hex"), } } } impl From for BlindSignatureDleq { fn from(dleq: cdk::nuts::BlindSignatureDleq) -> Self { Self { e: dleq.e.to_secret_hex(), s: dleq.s.to_secret_hex(), } } } impl From for cdk::nuts::BlindSignatureDleq { fn from(dleq: BlindSignatureDleq) -> Self { Self { e: cdk::nuts::SecretKey::from_hex(&dleq.e).expect("Invalid e hex"), s: cdk::nuts::SecretKey::from_hex(&dleq.s).expect("Invalid s hex"), } } } /// Helper functions for Proofs pub fn proofs_total_amount(proofs: &Proofs) -> Result { let cdk_proofs: Vec = proofs.iter().map(|p| p.inner.clone()).collect(); use cdk::nuts::ProofsMethods; Ok(cdk_proofs.total_amount()?.into()) } /// FFI-compatible MintQuote #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MintQuote { /// Quote ID pub id: String, /// Quote amount pub amount: Option, /// Currency unit pub unit: CurrencyUnit, /// Payment request pub request: String, /// Quote state pub state: QuoteState, /// Expiry timestamp pub expiry: u64, /// Mint URL pub mint_url: MintUrl, /// Amount issued pub amount_issued: Amount, /// Amount paid pub amount_paid: Amount, /// Payment method pub payment_method: PaymentMethod, /// Secret key (optional, hex-encoded) pub secret_key: Option, } impl From for MintQuote { fn from(quote: cdk::wallet::MintQuote) -> Self { Self { id: quote.id.clone(), amount: quote.amount.map(Into::into), unit: quote.unit.clone().into(), request: quote.request.clone(), state: quote.state.into(), expiry: quote.expiry, mint_url: quote.mint_url.clone().into(), amount_issued: quote.amount_issued.into(), amount_paid: quote.amount_paid.into(), payment_method: quote.payment_method.into(), secret_key: quote.secret_key.map(|sk| sk.to_secret_hex()), } } } impl TryFrom for cdk::wallet::MintQuote { type Error = FfiError; fn try_from(quote: MintQuote) -> Result { let secret_key = quote .secret_key .map(|hex| cdk::nuts::SecretKey::from_hex(&hex)) .transpose() .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?; Ok(Self { id: quote.id, amount: quote.amount.map(Into::into), unit: quote.unit.into(), request: quote.request, state: quote.state.into(), expiry: quote.expiry, mint_url: quote.mint_url.try_into()?, amount_issued: quote.amount_issued.into(), amount_paid: quote.amount_paid.into(), payment_method: quote.payment_method.into(), secret_key, }) } } impl MintQuote { /// Get total amount (amount + fees) pub fn total_amount(&self) -> Amount { if let Some(amount) = self.amount { Amount::new(amount.value + self.amount_paid.value - self.amount_issued.value) } else { Amount::zero() } } /// Check if quote is expired pub fn is_expired(&self, current_time: u64) -> bool { current_time > self.expiry } /// Get amount that can be minted pub fn amount_mintable(&self) -> Amount { Amount::new(self.amount_paid.value - self.amount_issued.value) } /// Convert MintQuote to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode MintQuote from JSON string #[uniffi::export] pub fn decode_mint_quote(json: String) -> Result { let quote: cdk::wallet::MintQuote = serde_json::from_str(&json)?; Ok(quote.into()) } /// Encode MintQuote to JSON string #[uniffi::export] pub fn encode_mint_quote(quote: MintQuote) -> Result { Ok(serde_json::to_string("e)?) } /// FFI-compatible MintQuoteBolt11Response #[derive(Debug, uniffi::Object)] pub struct MintQuoteBolt11Response { /// Quote ID pub quote: String, /// Request string pub request: String, /// State of the quote pub state: QuoteState, /// Expiry timestamp (optional) pub expiry: Option, /// Amount (optional) pub amount: Option, /// Unit (optional) pub unit: Option, /// Pubkey (optional) pub pubkey: Option, } impl From> for MintQuoteBolt11Response { fn from(response: cdk::nuts::MintQuoteBolt11Response) -> Self { Self { quote: response.quote, request: response.request, state: response.state.into(), expiry: response.expiry, amount: response.amount.map(Into::into), unit: response.unit.map(Into::into), pubkey: response.pubkey.map(|p| p.to_string()), } } } #[uniffi::export] impl MintQuoteBolt11Response { /// Get quote ID pub fn quote(&self) -> String { self.quote.clone() } /// Get request string pub fn request(&self) -> String { self.request.clone() } /// Get state pub fn state(&self) -> QuoteState { self.state.clone() } /// Get expiry pub fn expiry(&self) -> Option { self.expiry } /// Get amount pub fn amount(&self) -> Option { self.amount } /// Get unit pub fn unit(&self) -> Option { self.unit.clone() } /// Get pubkey pub fn pubkey(&self) -> Option { self.pubkey.clone() } } /// FFI-compatible MeltQuoteBolt11Response #[derive(Debug, uniffi::Object)] pub struct MeltQuoteBolt11Response { /// Quote ID pub quote: String, /// Amount pub amount: Amount, /// Fee reserve pub fee_reserve: Amount, /// State of the quote pub state: QuoteState, /// Expiry timestamp pub expiry: u64, /// Payment preimage (optional) pub payment_preimage: Option, /// Request string (optional) pub request: Option, /// Unit (optional) pub unit: Option, } impl From> for MeltQuoteBolt11Response { fn from(response: cdk::nuts::MeltQuoteBolt11Response) -> Self { Self { quote: response.quote, amount: response.amount.into(), fee_reserve: response.fee_reserve.into(), state: response.state.into(), expiry: response.expiry, payment_preimage: response.payment_preimage, request: response.request, unit: response.unit.map(Into::into), } } } #[uniffi::export] impl MeltQuoteBolt11Response { /// Get quote ID pub fn quote(&self) -> String { self.quote.clone() } /// Get amount pub fn amount(&self) -> Amount { self.amount } /// Get fee reserve pub fn fee_reserve(&self) -> Amount { self.fee_reserve } /// Get state pub fn state(&self) -> QuoteState { self.state.clone() } /// Get expiry pub fn expiry(&self) -> u64 { self.expiry } /// Get payment preimage pub fn payment_preimage(&self) -> Option { self.payment_preimage.clone() } /// Get request pub fn request(&self) -> Option { self.request.clone() } /// Get unit pub fn unit(&self) -> Option { self.unit.clone() } } /// FFI-compatible PaymentMethod #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum PaymentMethod { /// Bolt11 payment type Bolt11, /// Bolt12 payment type Bolt12, /// Custom payment type Custom { method: String }, } impl From for PaymentMethod { fn from(method: cdk::nuts::PaymentMethod) -> Self { match method { cdk::nuts::PaymentMethod::Bolt11 => Self::Bolt11, cdk::nuts::PaymentMethod::Bolt12 => Self::Bolt12, cdk::nuts::PaymentMethod::Custom(s) => Self::Custom { method: s }, } } } impl From for cdk::nuts::PaymentMethod { fn from(method: PaymentMethod) -> Self { match method { PaymentMethod::Bolt11 => Self::Bolt11, PaymentMethod::Bolt12 => Self::Bolt12, PaymentMethod::Custom { method } => Self::Custom(method), } } } /// FFI-compatible MeltQuote #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MeltQuote { /// Quote ID pub id: String, /// Quote amount pub amount: Amount, /// Currency unit pub unit: CurrencyUnit, /// Payment request pub request: String, /// Fee reserve pub fee_reserve: Amount, /// Quote state pub state: QuoteState, /// Expiry timestamp pub expiry: u64, /// Payment preimage pub payment_preimage: Option, /// Payment method pub payment_method: PaymentMethod, } impl From for MeltQuote { fn from(quote: cdk::wallet::MeltQuote) -> Self { Self { id: quote.id.clone(), amount: quote.amount.into(), unit: quote.unit.clone().into(), request: quote.request.clone(), fee_reserve: quote.fee_reserve.into(), state: quote.state.into(), expiry: quote.expiry, payment_preimage: quote.payment_preimage.clone(), payment_method: quote.payment_method.into(), } } } impl TryFrom for cdk::wallet::MeltQuote { type Error = FfiError; fn try_from(quote: MeltQuote) -> Result { Ok(Self { id: quote.id, amount: quote.amount.into(), unit: quote.unit.into(), request: quote.request, fee_reserve: quote.fee_reserve.into(), state: quote.state.into(), expiry: quote.expiry, payment_preimage: quote.payment_preimage, payment_method: quote.payment_method.into(), }) } } impl MeltQuote { /// Convert MeltQuote to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode MeltQuote from JSON string #[uniffi::export] pub fn decode_melt_quote(json: String) -> Result { let quote: cdk::wallet::MeltQuote = serde_json::from_str(&json)?; Ok(quote.into()) } /// Encode MeltQuote to JSON string #[uniffi::export] pub fn encode_melt_quote(quote: MeltQuote) -> Result { Ok(serde_json::to_string("e)?) } /// FFI-compatible QuoteState #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum QuoteState { Unpaid, Paid, Pending, Issued, } impl From for QuoteState { fn from(state: cdk::nuts::nut05::QuoteState) -> Self { match state { cdk::nuts::nut05::QuoteState::Unpaid => QuoteState::Unpaid, cdk::nuts::nut05::QuoteState::Paid => QuoteState::Paid, cdk::nuts::nut05::QuoteState::Pending => QuoteState::Pending, cdk::nuts::nut05::QuoteState::Unknown => QuoteState::Unpaid, cdk::nuts::nut05::QuoteState::Failed => QuoteState::Unpaid, } } } impl From for cdk::nuts::nut05::QuoteState { fn from(state: QuoteState) -> Self { match state { QuoteState::Unpaid => cdk::nuts::nut05::QuoteState::Unpaid, QuoteState::Paid => cdk::nuts::nut05::QuoteState::Paid, QuoteState::Pending => cdk::nuts::nut05::QuoteState::Pending, QuoteState::Issued => cdk::nuts::nut05::QuoteState::Paid, // Map issued to paid for melt quotes } } } impl From for QuoteState { fn from(state: cdk::nuts::MintQuoteState) -> Self { match state { cdk::nuts::MintQuoteState::Unpaid => QuoteState::Unpaid, cdk::nuts::MintQuoteState::Paid => QuoteState::Paid, cdk::nuts::MintQuoteState::Issued => QuoteState::Issued, } } } impl From for cdk::nuts::MintQuoteState { fn from(state: QuoteState) -> Self { match state { QuoteState::Unpaid => cdk::nuts::MintQuoteState::Unpaid, QuoteState::Paid => cdk::nuts::MintQuoteState::Paid, QuoteState::Issued => cdk::nuts::MintQuoteState::Issued, QuoteState::Pending => cdk::nuts::MintQuoteState::Paid, // Map pending to paid } } } // Note: MeltQuoteState is the same as nut05::QuoteState, so we don't need a separate impl /// FFI-compatible PreparedSend #[derive(Debug, uniffi::Object)] pub struct PreparedSend { inner: Mutex>, id: String, amount: Amount, proofs: Proofs, } impl From for PreparedSend { fn from(prepared: cdk::wallet::PreparedSend) -> Self { let id = format!("{:?}", prepared); // Use debug format as ID let amount = prepared.amount().into(); let proofs = prepared .proofs() .iter() .cloned() .map(|p| std::sync::Arc::new(p.into())) .collect(); Self { inner: Mutex::new(Some(prepared)), id, amount, proofs, } } } #[uniffi::export(async_runtime = "tokio")] impl PreparedSend { /// Get the prepared send ID pub fn id(&self) -> String { self.id.clone() } /// Get the amount to send pub fn amount(&self) -> Amount { self.amount } /// Get the proofs that will be used pub fn proofs(&self) -> Proofs { self.proofs.clone() } /// Get the total fee for this send operation pub fn fee(&self) -> Amount { if let Ok(guard) = self.inner.lock() { if let Some(ref inner) = *guard { inner.fee().into() } else { Amount::new(0) } } else { Amount::new(0) } } /// Confirm the prepared send and create a token pub async fn confirm( self: std::sync::Arc, memo: Option, ) -> Result { let inner = { if let Ok(mut guard) = self.inner.lock() { guard.take() } else { return Err(FfiError::Generic { msg: "Failed to acquire lock on PreparedSend".to_string(), }); } }; if let Some(inner) = inner { let send_memo = memo.map(|m| cdk::wallet::SendMemo::for_token(&m)); let token = inner.confirm(send_memo).await?; Ok(token.into()) } else { Err(FfiError::Generic { msg: "PreparedSend has already been consumed or cancelled".to_string(), }) } } /// Cancel the prepared send operation pub async fn cancel(self: std::sync::Arc) -> Result<(), FfiError> { let inner = { if let Ok(mut guard) = self.inner.lock() { guard.take() } else { return Err(FfiError::Generic { msg: "Failed to acquire lock on PreparedSend".to_string(), }); } }; if let Some(inner) = inner { inner.cancel().await?; Ok(()) } else { Err(FfiError::Generic { msg: "PreparedSend has already been consumed or cancelled".to_string(), }) } } } /// FFI-compatible Melted result #[derive(Debug, Clone, uniffi::Record)] pub struct Melted { pub state: QuoteState, pub preimage: Option, pub change: Option, pub amount: Amount, pub fee_paid: Amount, } // MeltQuoteState is just an alias for nut05::QuoteState, so we don't need a separate implementation impl From for Melted { fn from(melted: cdk::types::Melted) -> Self { Self { state: melted.state.into(), preimage: melted.preimage, change: melted.change.map(|proofs| { proofs .into_iter() .map(|p| std::sync::Arc::new(p.into())) .collect() }), amount: melted.amount.into(), fee_paid: melted.fee_paid.into(), } } } /// FFI-compatible MeltOptions #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] pub enum MeltOptions { /// MPP (Multi-Part Payments) options Mpp { amount: Amount }, /// Amountless options Amountless { amount_msat: Amount }, } impl From for cdk::nuts::MeltOptions { fn from(opts: MeltOptions) -> Self { match opts { MeltOptions::Mpp { amount } => { let cdk_amount: cdk::Amount = amount.into(); cdk::nuts::MeltOptions::new_mpp(cdk_amount) } MeltOptions::Amountless { amount_msat } => { let cdk_amount: cdk::Amount = amount_msat.into(); cdk::nuts::MeltOptions::new_amountless(cdk_amount) } } } } impl From for MeltOptions { fn from(opts: cdk::nuts::MeltOptions) -> Self { match opts { cdk::nuts::MeltOptions::Mpp { mpp } => MeltOptions::Mpp { amount: mpp.amount.into(), }, cdk::nuts::MeltOptions::Amountless { amountless } => MeltOptions::Amountless { amount_msat: amountless.amount_msat.into(), }, } } } /// FFI-compatible MintVersion #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MintVersion { /// Mint Software name pub name: String, /// Mint Version pub version: String, } impl From for MintVersion { fn from(version: cdk::nuts::MintVersion) -> Self { Self { name: version.name, version: version.version, } } } impl From for cdk::nuts::MintVersion { fn from(version: MintVersion) -> Self { Self { name: version.name, version: version.version, } } } impl MintVersion { /// Convert MintVersion to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode MintVersion from JSON string #[uniffi::export] pub fn decode_mint_version(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode MintVersion to JSON string #[uniffi::export] pub fn encode_mint_version(version: MintVersion) -> Result { Ok(serde_json::to_string(&version)?) } /// FFI-compatible ContactInfo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ContactInfo { /// Contact Method i.e. nostr pub method: String, /// Contact info i.e. npub... pub info: String, } impl From for ContactInfo { fn from(contact: cdk::nuts::ContactInfo) -> Self { Self { method: contact.method, info: contact.info, } } } impl From for cdk::nuts::ContactInfo { fn from(contact: ContactInfo) -> Self { Self { method: contact.method, info: contact.info, } } } impl ContactInfo { /// Convert ContactInfo to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode ContactInfo from JSON string #[uniffi::export] pub fn decode_contact_info(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode ContactInfo to JSON string #[uniffi::export] pub fn encode_contact_info(info: ContactInfo) -> Result { Ok(serde_json::to_string(&info)?) } /// FFI-compatible SupportedSettings #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct SupportedSettings { /// Setting supported pub supported: bool, } impl From for SupportedSettings { fn from(settings: cdk::nuts::nut06::SupportedSettings) -> Self { Self { supported: settings.supported, } } } impl From for cdk::nuts::nut06::SupportedSettings { fn from(settings: SupportedSettings) -> Self { Self { supported: settings.supported, } } } // ----------------------------- // NUT-04/05 FFI Types // ----------------------------- /// FFI-compatible MintMethodSettings (NUT-04) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MintMethodSettings { pub method: PaymentMethod, pub unit: CurrencyUnit, pub min_amount: Option, pub max_amount: Option, /// For bolt11, whether mint supports setting invoice description pub description: Option, } impl From for MintMethodSettings { fn from(s: cdk::nuts::nut04::MintMethodSettings) -> Self { let description = match s.options { Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) => Some(description), _ => None, }; Self { method: s.method.into(), unit: s.unit.into(), min_amount: s.min_amount.map(Into::into), max_amount: s.max_amount.map(Into::into), description, } } } impl TryFrom for cdk::nuts::nut04::MintMethodSettings { type Error = FfiError; fn try_from(s: MintMethodSettings) -> Result { let options = match (s.method.clone(), s.description) { (PaymentMethod::Bolt11, Some(description)) => { Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }) } _ => None, }; Ok(Self { method: s.method.into(), unit: s.unit.into(), min_amount: s.min_amount.map(Into::into), max_amount: s.max_amount.map(Into::into), options, }) } } /// FFI-compatible Nut04 Settings #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Nut04Settings { pub methods: Vec, pub disabled: bool, } impl From for Nut04Settings { fn from(s: cdk::nuts::nut04::Settings) -> Self { Self { methods: s.methods.into_iter().map(Into::into).collect(), disabled: s.disabled, } } } impl TryFrom for cdk::nuts::nut04::Settings { type Error = FfiError; fn try_from(s: Nut04Settings) -> Result { Ok(Self { methods: s .methods .into_iter() .map(TryInto::try_into) .collect::>()?, disabled: s.disabled, }) } } /// FFI-compatible MeltMethodSettings (NUT-05) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MeltMethodSettings { pub method: PaymentMethod, pub unit: CurrencyUnit, pub min_amount: Option, pub max_amount: Option, /// For bolt11, whether mint supports amountless invoices pub amountless: Option, } impl From for MeltMethodSettings { fn from(s: cdk::nuts::nut05::MeltMethodSettings) -> Self { let amountless = match s.options { Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) => Some(amountless), _ => None, }; Self { method: s.method.into(), unit: s.unit.into(), min_amount: s.min_amount.map(Into::into), max_amount: s.max_amount.map(Into::into), amountless, } } } impl TryFrom for cdk::nuts::nut05::MeltMethodSettings { type Error = FfiError; fn try_from(s: MeltMethodSettings) -> Result { let options = match (s.method.clone(), s.amountless) { (PaymentMethod::Bolt11, Some(amountless)) => { Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 { amountless }) } _ => None, }; Ok(Self { method: s.method.into(), unit: s.unit.into(), min_amount: s.min_amount.map(Into::into), max_amount: s.max_amount.map(Into::into), options, }) } } /// FFI-compatible Nut05 Settings #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Nut05Settings { pub methods: Vec, pub disabled: bool, } impl From for Nut05Settings { fn from(s: cdk::nuts::nut05::Settings) -> Self { Self { methods: s.methods.into_iter().map(Into::into).collect(), disabled: s.disabled, } } } impl TryFrom for cdk::nuts::nut05::Settings { type Error = FfiError; fn try_from(s: Nut05Settings) -> Result { Ok(Self { methods: s .methods .into_iter() .map(TryInto::try_into) .collect::>()?, disabled: s.disabled, }) } } /// FFI-compatible ProtectedEndpoint (for auth nuts) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ProtectedEndpoint { /// HTTP method (GET, POST, etc.) pub method: String, /// Endpoint path pub path: String, } /// FFI-compatible ClearAuthSettings (NUT-21) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ClearAuthSettings { /// OpenID Connect discovery URL pub openid_discovery: String, /// OAuth 2.0 client ID pub client_id: String, /// Protected endpoints requiring clear authentication pub protected_endpoints: Vec, } /// FFI-compatible BlindAuthSettings (NUT-22) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct BlindAuthSettings { /// Maximum number of blind auth tokens that can be minted per request pub bat_max_mint: u64, /// Protected endpoints requiring blind authentication pub protected_endpoints: Vec, } impl From for ClearAuthSettings { fn from(settings: cdk::nuts::ClearAuthSettings) -> Self { Self { openid_discovery: settings.openid_discovery, client_id: settings.client_id, protected_endpoints: settings .protected_endpoints .into_iter() .map(Into::into) .collect(), } } } impl TryFrom for cdk::nuts::ClearAuthSettings { type Error = FfiError; fn try_from(settings: ClearAuthSettings) -> Result { Ok(Self { openid_discovery: settings.openid_discovery, client_id: settings.client_id, protected_endpoints: settings .protected_endpoints .into_iter() .map(|e| e.try_into()) .collect::, _>>()?, }) } } impl From for BlindAuthSettings { fn from(settings: cdk::nuts::BlindAuthSettings) -> Self { Self { bat_max_mint: settings.bat_max_mint, protected_endpoints: settings .protected_endpoints .into_iter() .map(Into::into) .collect(), } } } impl TryFrom for cdk::nuts::BlindAuthSettings { type Error = FfiError; fn try_from(settings: BlindAuthSettings) -> Result { Ok(Self { bat_max_mint: settings.bat_max_mint, protected_endpoints: settings .protected_endpoints .into_iter() .map(|e| e.try_into()) .collect::, _>>()?, }) } } impl From for ProtectedEndpoint { fn from(endpoint: cdk::nuts::ProtectedEndpoint) -> Self { Self { method: match endpoint.method { cdk::nuts::Method::Get => "GET".to_string(), cdk::nuts::Method::Post => "POST".to_string(), }, path: endpoint.path.to_string(), } } } impl TryFrom for cdk::nuts::ProtectedEndpoint { type Error = FfiError; fn try_from(endpoint: ProtectedEndpoint) -> Result { let method = match endpoint.method.as_str() { "GET" => cdk::nuts::Method::Get, "POST" => cdk::nuts::Method::Post, _ => { return Err(FfiError::Generic { msg: format!( "Invalid HTTP method: {}. Only GET and POST are supported", endpoint.method ), }) } }; // Convert path string to RoutePath by matching against known paths let route_path = match endpoint.path.as_str() { "/v1/mint/quote/bolt11" => cdk::nuts::RoutePath::MintQuoteBolt11, "/v1/mint/bolt11" => cdk::nuts::RoutePath::MintBolt11, "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuoteBolt11, "/v1/melt/bolt11" => cdk::nuts::RoutePath::MeltBolt11, "/v1/swap" => cdk::nuts::RoutePath::Swap, "/v1/checkstate" => cdk::nuts::RoutePath::Checkstate, "/v1/restore" => cdk::nuts::RoutePath::Restore, "/v1/auth/blind/mint" => cdk::nuts::RoutePath::MintBlindAuth, "/v1/mint/quote/bolt12" => cdk::nuts::RoutePath::MintQuoteBolt12, "/v1/mint/bolt12" => cdk::nuts::RoutePath::MintBolt12, "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuoteBolt12, "/v1/melt/bolt12" => cdk::nuts::RoutePath::MeltBolt12, _ => { return Err(FfiError::Generic { msg: format!("Unknown route path: {}", endpoint.path), }) } }; Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path)) } } /// FFI-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Nuts { /// NUT04 Settings pub nut04: Nut04Settings, /// NUT05 Settings pub nut05: Nut05Settings, /// NUT07 Settings - Token state check pub nut07_supported: bool, /// NUT08 Settings - Lightning fee return pub nut08_supported: bool, /// NUT09 Settings - Restore signature pub nut09_supported: bool, /// NUT10 Settings - Spending conditions pub nut10_supported: bool, /// NUT11 Settings - Pay to Public Key Hash pub nut11_supported: bool, /// NUT12 Settings - DLEQ proofs pub nut12_supported: bool, /// NUT14 Settings - Hashed Time Locked Contracts pub nut14_supported: bool, /// NUT20 Settings - Web sockets pub nut20_supported: bool, /// NUT21 Settings - Clear authentication pub nut21: Option, /// NUT22 Settings - Blind authentication pub nut22: Option, /// Supported currency units for minting pub mint_units: Vec, /// Supported currency units for melting pub melt_units: Vec, } impl From for Nuts { fn from(nuts: cdk::nuts::Nuts) -> Self { let mint_units = nuts .supported_mint_units() .into_iter() .map(|u| u.clone().into()) .collect(); let melt_units = nuts .supported_melt_units() .into_iter() .map(|u| u.clone().into()) .collect(); Self { nut04: nuts.nut04.clone().into(), nut05: nuts.nut05.clone().into(), nut07_supported: nuts.nut07.supported, nut08_supported: nuts.nut08.supported, nut09_supported: nuts.nut09.supported, nut10_supported: nuts.nut10.supported, nut11_supported: nuts.nut11.supported, nut12_supported: nuts.nut12.supported, nut14_supported: nuts.nut14.supported, nut20_supported: nuts.nut20.supported, nut21: nuts.nut21.map(Into::into), nut22: nuts.nut22.map(Into::into), mint_units, melt_units, } } } impl TryFrom for cdk::nuts::Nuts { type Error = FfiError; fn try_from(n: Nuts) -> Result { Ok(Self { nut04: n.nut04.try_into()?, nut05: n.nut05.try_into()?, nut07: cdk::nuts::nut06::SupportedSettings { supported: n.nut07_supported, }, nut08: cdk::nuts::nut06::SupportedSettings { supported: n.nut08_supported, }, nut09: cdk::nuts::nut06::SupportedSettings { supported: n.nut09_supported, }, nut10: cdk::nuts::nut06::SupportedSettings { supported: n.nut10_supported, }, nut11: cdk::nuts::nut06::SupportedSettings { supported: n.nut11_supported, }, nut12: cdk::nuts::nut06::SupportedSettings { supported: n.nut12_supported, }, nut14: cdk::nuts::nut06::SupportedSettings { supported: n.nut14_supported, }, nut15: Default::default(), nut17: Default::default(), nut19: Default::default(), nut20: cdk::nuts::nut06::SupportedSettings { supported: n.nut20_supported, }, nut21: n.nut21.map(|s| s.try_into()).transpose()?, nut22: n.nut22.map(|s| s.try_into()).transpose()?, }) } } impl Nuts { /// Convert Nuts to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode Nuts from JSON string #[uniffi::export] pub fn decode_nuts(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode Nuts to JSON string #[uniffi::export] pub fn encode_nuts(nuts: Nuts) -> Result { Ok(serde_json::to_string(&nuts)?) } /// FFI-compatible MintInfo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct MintInfo { /// name of the mint and should be recognizable pub name: Option, /// hex pubkey of the mint pub pubkey: Option, /// implementation name and the version running pub version: Option, /// short description of the mint pub description: Option, /// long description pub description_long: Option, /// Contact info pub contact: Option>, /// shows which NUTs the mint supports pub nuts: Nuts, /// Mint's icon URL pub icon_url: Option, /// Mint's endpoint URLs pub urls: Option>, /// message of the day that the wallet must display to the user pub motd: Option, /// server unix timestamp pub time: Option, /// terms of url service of the mint pub tos_url: Option, } impl From for MintInfo { fn from(info: cdk::nuts::MintInfo) -> Self { Self { name: info.name, pubkey: info.pubkey.map(|p| p.to_string()), version: info.version.map(Into::into), description: info.description, description_long: info.description_long, contact: info .contact .map(|contacts| contacts.into_iter().map(Into::into).collect()), nuts: info.nuts.into(), icon_url: info.icon_url, urls: info.urls, motd: info.motd, time: info.time, tos_url: info.tos_url, } } } impl From for cdk::nuts::MintInfo { fn from(info: MintInfo) -> Self { // Convert FFI Nuts back to cdk::nuts::Nuts (best-effort) let nuts_cdk: cdk::nuts::Nuts = info.nuts.clone().try_into().unwrap_or_default(); Self { name: info.name, pubkey: info.pubkey.and_then(|p| p.parse().ok()), version: info.version.map(Into::into), description: info.description, description_long: info.description_long, contact: info .contact .map(|contacts| contacts.into_iter().map(Into::into).collect()), nuts: nuts_cdk, icon_url: info.icon_url, urls: info.urls, motd: info.motd, time: info.time, tos_url: info.tos_url, } } } impl MintInfo { /// Convert MintInfo to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode MintInfo from JSON string #[uniffi::export] pub fn decode_mint_info(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode MintInfo to JSON string #[uniffi::export] pub fn encode_mint_info(info: MintInfo) -> Result { Ok(serde_json::to_string(&info)?) } /// FFI-compatible Conditions (for spending conditions) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Conditions { /// Unix locktime after which refund keys can be used pub locktime: Option, /// Additional Public keys (as hex strings) pub pubkeys: Vec, /// Refund keys (as hex strings) pub refund_keys: Vec, /// Number of signatures required (default 1) pub num_sigs: Option, /// Signature flag (0 = SigInputs, 1 = SigAll) pub sig_flag: u8, /// Number of refund signatures required (default 1) pub num_sigs_refund: Option, } impl From for Conditions { fn from(conditions: cdk::nuts::nut11::Conditions) -> Self { Self { locktime: conditions.locktime, pubkeys: conditions .pubkeys .unwrap_or_default() .into_iter() .map(|p| p.to_string()) .collect(), refund_keys: conditions .refund_keys .unwrap_or_default() .into_iter() .map(|p| p.to_string()) .collect(), num_sigs: conditions.num_sigs, sig_flag: match conditions.sig_flag { cdk::nuts::nut11::SigFlag::SigInputs => 0, cdk::nuts::nut11::SigFlag::SigAll => 1, }, num_sigs_refund: conditions.num_sigs_refund, } } } impl TryFrom for cdk::nuts::nut11::Conditions { type Error = FfiError; fn try_from(conditions: Conditions) -> Result { let pubkeys = if conditions.pubkeys.is_empty() { None } else { Some( conditions .pubkeys .into_iter() .map(|s| { s.parse().map_err(|e| FfiError::InvalidCryptographicKey { msg: format!("Invalid pubkey: {}", e), }) }) .collect::, _>>()?, ) }; let refund_keys = if conditions.refund_keys.is_empty() { None } else { Some( conditions .refund_keys .into_iter() .map(|s| { s.parse().map_err(|e| FfiError::InvalidCryptographicKey { msg: format!("Invalid refund key: {}", e), }) }) .collect::, _>>()?, ) }; let sig_flag = match conditions.sig_flag { 0 => cdk::nuts::nut11::SigFlag::SigInputs, 1 => cdk::nuts::nut11::SigFlag::SigAll, _ => { return Err(FfiError::Generic { msg: "Invalid sig_flag value".to_string(), }) } }; Ok(Self { locktime: conditions.locktime, pubkeys, refund_keys, num_sigs: conditions.num_sigs, sig_flag, num_sigs_refund: conditions.num_sigs_refund, }) } } impl Conditions { /// Convert Conditions to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode Conditions from JSON string #[uniffi::export] pub fn decode_conditions(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode Conditions to JSON string #[uniffi::export] pub fn encode_conditions(conditions: Conditions) -> Result { Ok(serde_json::to_string(&conditions)?) } /// FFI-compatible Witness #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] pub enum Witness { /// P2PK Witness P2PK { /// Signatures signatures: Vec, }, /// HTLC Witness HTLC { /// Preimage preimage: String, /// Optional signatures signatures: Option>, }, } impl From for Witness { fn from(witness: cdk::nuts::Witness) -> Self { match witness { cdk::nuts::Witness::P2PKWitness(p2pk) => Self::P2PK { signatures: p2pk.signatures, }, cdk::nuts::Witness::HTLCWitness(htlc) => Self::HTLC { preimage: htlc.preimage, signatures: htlc.signatures, }, } } } impl From for cdk::nuts::Witness { fn from(witness: Witness) -> Self { match witness { Witness::P2PK { signatures } => { Self::P2PKWitness(cdk::nuts::nut11::P2PKWitness { signatures }) } Witness::HTLC { preimage, signatures, } => Self::HTLCWitness(cdk::nuts::nut14::HTLCWitness { preimage, signatures, }), } } } /// FFI-compatible SpendingConditions #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] pub enum SpendingConditions { /// P2PK (Pay to Public Key) conditions P2PK { /// The public key (as hex string) pubkey: String, /// Additional conditions conditions: Option, }, /// HTLC (Hash Time Locked Contract) conditions HTLC { /// Hash of the preimage (as hex string) hash: String, /// Additional conditions conditions: Option, }, } impl From for SpendingConditions { fn from(spending_conditions: cdk::nuts::SpendingConditions) -> Self { match spending_conditions { cdk::nuts::SpendingConditions::P2PKConditions { data, conditions } => Self::P2PK { pubkey: data.to_string(), conditions: conditions.map(Into::into), }, cdk::nuts::SpendingConditions::HTLCConditions { data, conditions } => Self::HTLC { hash: data.to_string(), conditions: conditions.map(Into::into), }, } } } /// FFI-compatible Transaction #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Transaction { /// Transaction ID pub id: TransactionId, /// Mint URL pub mint_url: MintUrl, /// Transaction direction pub direction: TransactionDirection, /// Amount pub amount: Amount, /// Fee pub fee: Amount, /// Currency Unit pub unit: CurrencyUnit, /// Proof Ys (Y values from proofs) pub ys: Vec, /// Unix timestamp pub timestamp: u64, /// Memo pub memo: Option, /// User-defined metadata pub metadata: HashMap, /// Quote ID if this is a mint or melt transaction pub quote_id: Option, } impl From for Transaction { fn from(tx: cdk::wallet::types::Transaction) -> Self { Self { id: tx.id().into(), mint_url: tx.mint_url.into(), direction: tx.direction.into(), amount: tx.amount.into(), fee: tx.fee.into(), unit: tx.unit.into(), ys: tx.ys.into_iter().map(Into::into).collect(), timestamp: tx.timestamp, memo: tx.memo, metadata: tx.metadata, quote_id: tx.quote_id, } } } /// Convert FFI Transaction to CDK Transaction impl TryFrom for cdk::wallet::types::Transaction { type Error = FfiError; fn try_from(tx: Transaction) -> Result { let cdk_ys: Result, _> = tx.ys.into_iter().map(|pk| pk.try_into()).collect(); let cdk_ys = cdk_ys?; Ok(Self { mint_url: tx.mint_url.try_into()?, direction: tx.direction.into(), amount: tx.amount.into(), fee: tx.fee.into(), unit: tx.unit.into(), ys: cdk_ys, timestamp: tx.timestamp, memo: tx.memo, metadata: tx.metadata, quote_id: tx.quote_id, }) } } impl Transaction { /// Convert Transaction to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode Transaction from JSON string #[uniffi::export] pub fn decode_transaction(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode Transaction to JSON string #[uniffi::export] pub fn encode_transaction(transaction: Transaction) -> Result { Ok(serde_json::to_string(&transaction)?) } /// FFI-compatible TransactionDirection #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum TransactionDirection { /// Incoming transaction (i.e., receive or mint) Incoming, /// Outgoing transaction (i.e., send or melt) Outgoing, } impl From for TransactionDirection { fn from(direction: cdk::wallet::types::TransactionDirection) -> Self { match direction { cdk::wallet::types::TransactionDirection::Incoming => TransactionDirection::Incoming, cdk::wallet::types::TransactionDirection::Outgoing => TransactionDirection::Outgoing, } } } impl From for cdk::wallet::types::TransactionDirection { fn from(direction: TransactionDirection) -> Self { match direction { TransactionDirection::Incoming => cdk::wallet::types::TransactionDirection::Incoming, TransactionDirection::Outgoing => cdk::wallet::types::TransactionDirection::Outgoing, } } } /// FFI-compatible TransactionId #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct TransactionId { /// Hex-encoded transaction ID (64 characters) pub hex: String, } impl TransactionId { /// Create a new TransactionId from hex string pub fn from_hex(hex: String) -> Result { // Validate hex string length (should be 64 characters for 32 bytes) if hex.len() != 64 { return Err(FfiError::InvalidHex { msg: "Transaction ID hex must be exactly 64 characters (32 bytes)".to_string(), }); } // Validate hex format if !hex.chars().all(|c| c.is_ascii_hexdigit()) { return Err(FfiError::InvalidHex { msg: "Transaction ID hex contains invalid characters".to_string(), }); } Ok(Self { hex }) } /// Create from proofs pub fn from_proofs(proofs: &Proofs) -> Result { let cdk_proofs: Vec = proofs.iter().map(|p| p.inner.clone()).collect(); let id = cdk::wallet::types::TransactionId::from_proofs(cdk_proofs)?; Ok(Self { hex: id.to_string(), }) } } impl From for TransactionId { fn from(id: cdk::wallet::types::TransactionId) -> Self { Self { hex: id.to_string(), } } } impl TryFrom for cdk::wallet::types::TransactionId { type Error = FfiError; fn try_from(id: TransactionId) -> Result { cdk::wallet::types::TransactionId::from_hex(&id.hex) .map_err(|e| FfiError::InvalidHex { msg: e.to_string() }) } } /// FFI-compatible AuthProof #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct AuthProof { /// Keyset ID pub keyset_id: String, /// Secret message pub secret: String, /// Unblinded signature (C) pub c: String, /// Y value (hash_to_curve of secret) pub y: String, } impl From for AuthProof { fn from(auth_proof: cdk::nuts::AuthProof) -> Self { Self { keyset_id: auth_proof.keyset_id.to_string(), secret: auth_proof.secret.to_string(), c: auth_proof.c.to_string(), y: auth_proof .y() .map(|y| y.to_string()) .unwrap_or_else(|_| "".to_string()), } } } impl TryFrom for cdk::nuts::AuthProof { type Error = FfiError; fn try_from(auth_proof: AuthProof) -> Result { use std::str::FromStr; Ok(Self { keyset_id: cdk::nuts::Id::from_str(&auth_proof.keyset_id) .map_err(|e| FfiError::Serialization { msg: e.to_string() })?, secret: { use std::str::FromStr; cdk::secret::Secret::from_str(&auth_proof.secret) .map_err(|e| FfiError::Serialization { msg: e.to_string() })? }, c: cdk::nuts::PublicKey::from_str(&auth_proof.c) .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?, dleq: None, // FFI doesn't expose DLEQ proofs for simplicity }) } } impl AuthProof { /// Convert AuthProof to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode AuthProof from JSON string #[uniffi::export] pub fn decode_auth_proof(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode AuthProof to JSON string #[uniffi::export] pub fn encode_auth_proof(proof: AuthProof) -> Result { Ok(serde_json::to_string(&proof)?) } impl TryFrom for cdk::nuts::SpendingConditions { type Error = FfiError; fn try_from(spending_conditions: SpendingConditions) -> Result { match spending_conditions { SpendingConditions::P2PK { pubkey, conditions } => { let pubkey = pubkey .parse() .map_err(|e| FfiError::InvalidCryptographicKey { msg: format!("Invalid pubkey: {}", e), })?; let conditions = conditions.map(|c| c.try_into()).transpose()?; Ok(Self::P2PKConditions { data: pubkey, conditions, }) } SpendingConditions::HTLC { hash, conditions } => { let hash = hash .parse() .map_err(|e| FfiError::InvalidCryptographicKey { msg: format!("Invalid hash: {}", e), })?; let conditions = conditions.map(|c| c.try_into()).transpose()?; Ok(Self::HTLCConditions { data: hash, conditions, }) } } } } /// FFI-compatible SubscriptionKind #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] pub enum SubscriptionKind { /// Bolt 11 Melt Quote Bolt11MeltQuote, /// Bolt 11 Mint Quote Bolt11MintQuote, /// Bolt 12 Mint Quote Bolt12MintQuote, /// Proof State ProofState, } impl From for cdk::nuts::nut17::Kind { fn from(kind: SubscriptionKind) -> Self { match kind { SubscriptionKind::Bolt11MeltQuote => cdk::nuts::nut17::Kind::Bolt11MeltQuote, SubscriptionKind::Bolt11MintQuote => cdk::nuts::nut17::Kind::Bolt11MintQuote, SubscriptionKind::Bolt12MintQuote => cdk::nuts::nut17::Kind::Bolt12MintQuote, SubscriptionKind::ProofState => cdk::nuts::nut17::Kind::ProofState, } } } impl From for SubscriptionKind { fn from(kind: cdk::nuts::nut17::Kind) -> Self { match kind { cdk::nuts::nut17::Kind::Bolt11MeltQuote => SubscriptionKind::Bolt11MeltQuote, cdk::nuts::nut17::Kind::Bolt11MintQuote => SubscriptionKind::Bolt11MintQuote, cdk::nuts::nut17::Kind::Bolt12MintQuote => SubscriptionKind::Bolt12MintQuote, cdk::nuts::nut17::Kind::ProofState => SubscriptionKind::ProofState, } } } /// FFI-compatible SubscribeParams #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct SubscribeParams { /// Subscription kind pub kind: SubscriptionKind, /// Filters pub filters: Vec, /// Subscription ID (optional, will be generated if not provided) pub id: Option, } impl From for cdk::nuts::nut17::Params { fn from(params: SubscribeParams) -> Self { let sub_id = params .id .map(|id| SubId::from(id.as_str())) .unwrap_or_else(|| { // Generate a random ID let uuid = uuid::Uuid::new_v4(); SubId::from(uuid.to_string().as_str()) }); cdk::nuts::nut17::Params { kind: params.kind.into(), filters: params.filters, id: sub_id, } } } impl SubscribeParams { /// Convert SubscribeParams to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode SubscribeParams from JSON string #[uniffi::export] pub fn decode_subscribe_params(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode SubscribeParams to JSON string #[uniffi::export] pub fn encode_subscribe_params(params: SubscribeParams) -> Result { Ok(serde_json::to_string(¶ms)?) } /// FFI-compatible ActiveSubscription #[derive(uniffi::Object)] pub struct ActiveSubscription { inner: std::sync::Arc>, pub sub_id: String, } impl ActiveSubscription { pub(crate) fn new( inner: cdk::wallet::subscription::ActiveSubscription, sub_id: String, ) -> Self { Self { inner: std::sync::Arc::new(tokio::sync::Mutex::new(inner)), sub_id, } } } #[uniffi::export(async_runtime = "tokio")] impl ActiveSubscription { /// Get the subscription ID pub fn id(&self) -> String { self.sub_id.clone() } /// Receive the next notification pub async fn recv(&self) -> Result { let mut guard = self.inner.lock().await; guard .recv() .await .ok_or(FfiError::Generic { msg: "Subscription closed".to_string(), }) .map(Into::into) } /// Try to receive a notification without blocking pub async fn try_recv(&self) -> Result, FfiError> { let mut guard = self.inner.lock().await; guard .try_recv() .map(|opt| opt.map(Into::into)) .map_err(|e| FfiError::Generic { msg: format!("Failed to receive notification: {}", e), }) } } /// FFI-compatible NotificationPayload #[derive(Debug, Clone, uniffi::Enum)] pub enum NotificationPayload { /// Proof state update ProofState { proof_states: Vec }, /// Mint quote update MintQuoteUpdate { quote: std::sync::Arc, }, /// Melt quote update MeltQuoteUpdate { quote: std::sync::Arc, }, } impl From> for NotificationPayload { fn from(payload: cdk::nuts::NotificationPayload) -> Self { match payload { cdk::nuts::NotificationPayload::ProofState(states) => NotificationPayload::ProofState { proof_states: vec![states.into()], }, cdk::nuts::NotificationPayload::MintQuoteBolt11Response(quote_resp) => { NotificationPayload::MintQuoteUpdate { quote: std::sync::Arc::new(quote_resp.into()), } } cdk::nuts::NotificationPayload::MeltQuoteBolt11Response(quote_resp) => { NotificationPayload::MeltQuoteUpdate { quote: std::sync::Arc::new(quote_resp.into()), } } _ => { // For now, handle other notification types as empty ProofState NotificationPayload::ProofState { proof_states: vec![], } } } } } /// FFI-compatible ProofStateUpdate #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ProofStateUpdate { /// Y value (hash_to_curve of secret) pub y: String, /// Current state pub state: ProofState, /// Optional witness data pub witness: Option, } impl From for ProofStateUpdate { fn from(proof_state: cdk::nuts::nut07::ProofState) -> Self { Self { y: proof_state.y.to_string(), state: proof_state.state.into(), witness: proof_state.witness.map(|w| format!("{:?}", w)), } } } impl ProofStateUpdate { /// Convert ProofStateUpdate to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode ProofStateUpdate from JSON string #[uniffi::export] pub fn decode_proof_state_update(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode ProofStateUpdate to JSON string #[uniffi::export] pub fn encode_proof_state_update(update: ProofStateUpdate) -> Result { Ok(serde_json::to_string(&update)?) } /// FFI-compatible KeySetInfo #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct KeySetInfo { pub id: String, pub unit: CurrencyUnit, pub active: bool, /// Input fee per thousand (ppk) pub input_fee_ppk: u64, } impl From for KeySetInfo { fn from(keyset: cdk::nuts::KeySetInfo) -> Self { Self { id: keyset.id.to_string(), unit: keyset.unit.into(), active: keyset.active, input_fee_ppk: keyset.input_fee_ppk, } } } impl From for cdk::nuts::KeySetInfo { fn from(keyset: KeySetInfo) -> Self { use std::str::FromStr; Self { id: cdk::nuts::Id::from_str(&keyset.id).unwrap(), unit: keyset.unit.into(), active: keyset.active, final_expiry: None, input_fee_ppk: keyset.input_fee_ppk, } } } impl KeySetInfo { /// Convert KeySetInfo to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode KeySetInfo from JSON string #[uniffi::export] pub fn decode_key_set_info(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode KeySetInfo to JSON string #[uniffi::export] pub fn encode_key_set_info(info: KeySetInfo) -> Result { Ok(serde_json::to_string(&info)?) } /// FFI-compatible PublicKey #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct PublicKey { /// Hex-encoded public key pub hex: String, } impl From for PublicKey { fn from(key: cdk::nuts::PublicKey) -> Self { Self { hex: key.to_string(), } } } impl TryFrom for cdk::nuts::PublicKey { type Error = FfiError; fn try_from(key: PublicKey) -> Result { key.hex .parse() .map_err(|e| FfiError::InvalidCryptographicKey { msg: format!("Invalid public key: {}", e), }) } } /// FFI-compatible Keys (simplified - contains only essential info) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct Keys { /// Keyset ID pub id: String, /// Currency unit pub unit: CurrencyUnit, /// Map of amount to public key hex (simplified from BTreeMap) pub keys: HashMap, } impl From for Keys { fn from(keys: cdk::nuts::Keys) -> Self { // Keys doesn't have id and unit - we'll need to get these from context // For now, use placeholder values Self { id: "unknown".to_string(), // This should come from KeySet unit: CurrencyUnit::Sat, // This should come from KeySet keys: keys .keys() .iter() .map(|(amount, pubkey)| (u64::from(*amount), pubkey.to_string())) .collect(), } } } impl TryFrom for cdk::nuts::Keys { type Error = FfiError; fn try_from(keys: Keys) -> Result { use std::collections::BTreeMap; use std::str::FromStr; // Convert the HashMap to BTreeMap with proper types let mut keys_map = BTreeMap::new(); for (amount_u64, pubkey_hex) in keys.keys { let amount = cdk::Amount::from(amount_u64); let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex) .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?; keys_map.insert(amount, pubkey); } Ok(cdk::nuts::Keys::new(keys_map)) } } impl Keys { /// Convert Keys to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode Keys from JSON string #[uniffi::export] pub fn decode_keys(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode Keys to JSON string #[uniffi::export] pub fn encode_keys(keys: Keys) -> Result { Ok(serde_json::to_string(&keys)?) } /// FFI-compatible KeySet #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct KeySet { /// Keyset ID pub id: String, /// Currency unit pub unit: CurrencyUnit, /// The keys (map of amount to public key hex) pub keys: HashMap, /// Optional expiry timestamp pub final_expiry: Option, } impl From for KeySet { fn from(keyset: cdk::nuts::KeySet) -> Self { Self { id: keyset.id.to_string(), unit: keyset.unit.into(), keys: keyset .keys .keys() .iter() .map(|(amount, pubkey)| (u64::from(*amount), pubkey.to_string())) .collect(), final_expiry: keyset.final_expiry, } } } impl TryFrom for cdk::nuts::KeySet { type Error = FfiError; fn try_from(keyset: KeySet) -> Result { use std::collections::BTreeMap; use std::str::FromStr; // Convert id let id = cdk::nuts::Id::from_str(&keyset.id) .map_err(|e| FfiError::Serialization { msg: e.to_string() })?; // Convert unit let unit: cdk::nuts::CurrencyUnit = keyset.unit.into(); // Convert keys let mut keys_map = BTreeMap::new(); for (amount_u64, pubkey_hex) in keyset.keys { let amount = cdk::Amount::from(amount_u64); let pubkey = cdk::nuts::PublicKey::from_str(&pubkey_hex) .map_err(|e| FfiError::InvalidCryptographicKey { msg: e.to_string() })?; keys_map.insert(amount, pubkey); } let keys = cdk::nuts::Keys::new(keys_map); Ok(cdk::nuts::KeySet { id, unit, keys, final_expiry: keyset.final_expiry, }) } } impl KeySet { /// Convert KeySet to JSON string pub fn to_json(&self) -> Result { Ok(serde_json::to_string(self)?) } } /// Decode KeySet from JSON string #[uniffi::export] pub fn decode_key_set(json: String) -> Result { Ok(serde_json::from_str(&json)?) } /// Encode KeySet to JSON string #[uniffi::export] pub fn encode_key_set(keyset: KeySet) -> Result { Ok(serde_json::to_string(&keyset)?) } /// FFI-compatible ProofInfo #[derive(Debug, Clone, uniffi::Record)] pub struct ProofInfo { /// Proof pub proof: std::sync::Arc, /// Y value (hash_to_curve of secret) pub y: PublicKey, /// Mint URL pub mint_url: MintUrl, /// Proof state pub state: ProofState, /// Proof Spending Conditions pub spending_condition: Option, /// Currency unit pub unit: CurrencyUnit, } impl From for ProofInfo { fn from(info: cdk::types::ProofInfo) -> Self { Self { proof: std::sync::Arc::new(info.proof.into()), y: info.y.into(), mint_url: info.mint_url.into(), state: info.state.into(), spending_condition: info.spending_condition.map(Into::into), unit: info.unit.into(), } } } /// Decode ProofInfo from JSON string #[uniffi::export] pub fn decode_proof_info(json: String) -> Result { let info: cdk::types::ProofInfo = serde_json::from_str(&json)?; Ok(info.into()) } /// Encode ProofInfo to JSON string #[uniffi::export] pub fn encode_proof_info(info: ProofInfo) -> Result { // Convert to cdk::types::ProofInfo for serialization let cdk_info = cdk::types::ProofInfo { proof: info.proof.inner.clone(), y: info.y.try_into()?, mint_url: info.mint_url.try_into()?, state: info.state.into(), spending_condition: info.spending_condition.and_then(|c| c.try_into().ok()), unit: info.unit.into(), }; Ok(serde_json::to_string(&cdk_info)?) } // State enum removed - using ProofState instead /// FFI-compatible Id (for keyset IDs) #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct Id { pub hex: String, } impl From for Id { fn from(id: cdk::nuts::Id) -> Self { Self { hex: id.to_string(), } } } impl From for cdk::nuts::Id { fn from(id: Id) -> Self { use std::str::FromStr; Self::from_str(&id.hex).unwrap() } }