diff --git a/crates/cdk-ffi/src/types.rs b/crates/cdk-ffi/src/types.rs deleted file mode 100644 index 1a6d1d4d..00000000 --- a/crates/cdk-ffi/src/types.rs +++ /dev/null @@ -1,2883 +0,0 @@ -//! 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; -use crate::token::Token; - -/// 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 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() - } -} diff --git a/crates/cdk-ffi/src/types/amount.rs b/crates/cdk-ffi/src/types/amount.rs new file mode 100644 index 00000000..d0864d57 --- /dev/null +++ b/crates/cdk-ffi/src/types/amount.rs @@ -0,0 +1,166 @@ +//! Amount and currency related types + +use cdk::nuts::CurrencyUnit as CdkCurrencyUnit; +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 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(), + }, + } + } +} diff --git a/crates/cdk-ffi/src/types/keys.rs b/crates/cdk-ffi/src/types/keys.rs new file mode 100644 index 00000000..57ef4761 --- /dev/null +++ b/crates/cdk-ffi/src/types/keys.rs @@ -0,0 +1,258 @@ +//! Key-related FFI types + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::amount::CurrencyUnit; +use crate::error::FfiError; + +/// 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 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() + } +} diff --git a/crates/cdk-ffi/src/types/mint.rs b/crates/cdk-ffi/src/types/mint.rs new file mode 100644 index 00000000..06236cbe --- /dev/null +++ b/crates/cdk-ffi/src/types/mint.rs @@ -0,0 +1,675 @@ +//! Mint-related FFI types + +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use super::amount::{Amount, CurrencyUnit}; +use super::quote::PaymentMethod; +use crate::error::FfiError; + +/// 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 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)?) +} diff --git a/crates/cdk-ffi/src/types/mod.rs b/crates/cdk-ffi/src/types/mod.rs new file mode 100644 index 00000000..3c425718 --- /dev/null +++ b/crates/cdk-ffi/src/types/mod.rs @@ -0,0 +1,24 @@ +//! FFI-compatible types +//! +//! This module contains all the FFI types used by the UniFFI bindings. +//! Types are organized into logical submodules for better maintainability. + +// Module declarations +pub mod amount; +pub mod keys; +pub mod mint; +pub mod proof; +pub mod quote; +pub mod subscription; +pub mod transaction; +pub mod wallet; + +// Re-export all types for convenient access +pub use amount::*; +pub use keys::*; +pub use mint::*; +pub use proof::*; +pub use quote::*; +pub use subscription::*; +pub use transaction::*; +pub use wallet::*; diff --git a/crates/cdk-ffi/src/types/proof.rs b/crates/cdk-ffi/src/types/proof.rs new file mode 100644 index 00000000..e75e6c0e --- /dev/null +++ b/crates/cdk-ffi/src/types/proof.rs @@ -0,0 +1,509 @@ +//! Proof-related FFI types + +use std::str::FromStr; + +use cdk::nuts::State as CdkState; +use serde::{Deserialize, Serialize}; + +use super::amount::{Amount, CurrencyUnit}; +use super::mint::MintUrl; +use crate::error::FfiError; + +/// 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 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 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), + }, + } + } +} + +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 ProofInfo +#[derive(Debug, Clone, uniffi::Record)] +pub struct ProofInfo { + /// Proof + pub proof: std::sync::Arc, + /// Y value (hash_to_curve of secret) + pub y: super::keys::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)?) +} + +/// 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)?) +} diff --git a/crates/cdk-ffi/src/types/quote.rs b/crates/cdk-ffi/src/types/quote.rs new file mode 100644 index 00000000..e931008a --- /dev/null +++ b/crates/cdk-ffi/src/types/quote.rs @@ -0,0 +1,430 @@ +//! Quote-related FFI types + +use serde::{Deserialize, Serialize}; + +use super::amount::{Amount, CurrencyUnit}; +use super::mint::MintUrl; +use crate::error::FfiError; + +/// 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 diff --git a/crates/cdk-ffi/src/types/subscription.rs b/crates/cdk-ffi/src/types/subscription.rs new file mode 100644 index 00000000..fb07389d --- /dev/null +++ b/crates/cdk-ffi/src/types/subscription.rs @@ -0,0 +1,183 @@ +//! Subscription-related FFI types + +use cdk::pub_sub::SubId; +use serde::{Deserialize, Serialize}; + +use super::proof::ProofStateUpdate; +use super::quote::{MeltQuoteBolt11Response, MintQuoteBolt11Response}; +use crate::error::FfiError; + +/// 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![], + } + } + } + } +} diff --git a/crates/cdk-ffi/src/types/transaction.rs b/crates/cdk-ffi/src/types/transaction.rs new file mode 100644 index 00000000..9291c778 --- /dev/null +++ b/crates/cdk-ffi/src/types/transaction.rs @@ -0,0 +1,247 @@ +//! Transaction-related FFI types + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::amount::{Amount, CurrencyUnit}; +use super::keys::PublicKey; +use super::mint::MintUrl; +use super::proof::Proofs; +use crate::error::FfiError; + +/// 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)?) +} diff --git a/crates/cdk-ffi/src/types/wallet.rs b/crates/cdk-ffi/src/types/wallet.rs new file mode 100644 index 00000000..336ca7f5 --- /dev/null +++ b/crates/cdk-ffi/src/types/wallet.rs @@ -0,0 +1,471 @@ +//! Wallet-related FFI types + +use std::collections::HashMap; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; + +use super::amount::{Amount, SplitTarget}; +use super::proof::{Proofs, SpendingConditions}; +use crate::error::FfiError; +use crate::token::Token; + +/// 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 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 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: super::quote::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(), + }, + } + } +}