Split uniffi types into multiple mods (#1142)

This commit is contained in:
David Caseria
2025-10-05 07:15:33 -04:00
committed by GitHub
parent e581dcbf62
commit a8c35dbef0
10 changed files with 2963 additions and 2883 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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<Amount, FfiError> {
Ok(CdkAmount::from(self.value)
.convert_unit(&current_unit.into(), &target_unit.into())
.map(Into::into)?)
}
pub fn add(&self, other: Amount) -> Result<Amount, FfiError> {
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<Amount, FfiError> {
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<Amount, FfiError> {
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<Amount, FfiError> {
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<CdkAmount> for Amount {
fn from(amount: CdkAmount) -> Self {
Self {
value: u64::from(amount),
}
}
}
impl From<Amount> 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<CdkCurrencyUnit> 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<CurrencyUnit> 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<Amount> },
}
impl From<SplitTarget> 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<cdk::amount::SplitTarget> 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(),
},
}
}
}

View File

@@ -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<cdk::nuts::KeySetInfo> 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<KeySetInfo> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode KeySetInfo from JSON string
#[uniffi::export]
pub fn decode_key_set_info(json: String) -> Result<KeySetInfo, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode KeySetInfo to JSON string
#[uniffi::export]
pub fn encode_key_set_info(info: KeySetInfo) -> Result<String, FfiError> {
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<cdk::nuts::PublicKey> for PublicKey {
fn from(key: cdk::nuts::PublicKey) -> Self {
Self {
hex: key.to_string(),
}
}
}
impl TryFrom<PublicKey> for cdk::nuts::PublicKey {
type Error = FfiError;
fn try_from(key: PublicKey) -> Result<Self, Self::Error> {
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<u64, String>,
}
impl From<cdk::nuts::Keys> 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<Keys> for cdk::nuts::Keys {
type Error = FfiError;
fn try_from(keys: Keys) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode Keys from JSON string
#[uniffi::export]
pub fn decode_keys(json: String) -> Result<Keys, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode Keys to JSON string
#[uniffi::export]
pub fn encode_keys(keys: Keys) -> Result<String, FfiError> {
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<u64, String>,
/// Optional expiry timestamp
pub final_expiry: Option<u64>,
}
impl From<cdk::nuts::KeySet> 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<KeySet> for cdk::nuts::KeySet {
type Error = FfiError;
fn try_from(keyset: KeySet) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode KeySet from JSON string
#[uniffi::export]
pub fn decode_key_set(json: String) -> Result<KeySet, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode KeySet to JSON string
#[uniffi::export]
pub fn encode_key_set(keyset: KeySet) -> Result<String, FfiError> {
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<cdk::nuts::Id> for Id {
fn from(id: cdk::nuts::Id) -> Self {
Self {
hex: id.to_string(),
}
}
}
impl From<Id> for cdk::nuts::Id {
fn from(id: Id) -> Self {
use std::str::FromStr;
Self::from_str(&id.hex).unwrap()
}
}

View File

@@ -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<Self, FfiError> {
// Validate URL format
url::Url::parse(&url).map_err(|e| FfiError::InvalidUrl { msg: e.to_string() })?;
Ok(Self { url })
}
}
impl From<cdk::mint_url::MintUrl> for MintUrl {
fn from(mint_url: cdk::mint_url::MintUrl) -> Self {
Self {
url: mint_url.to_string(),
}
}
}
impl TryFrom<MintUrl> for cdk::mint_url::MintUrl {
type Error = FfiError;
fn try_from(mint_url: MintUrl) -> Result<Self, Self::Error> {
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<cdk::nuts::MintVersion> for MintVersion {
fn from(version: cdk::nuts::MintVersion) -> Self {
Self {
name: version.name,
version: version.version,
}
}
}
impl From<MintVersion> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode MintVersion from JSON string
#[uniffi::export]
pub fn decode_mint_version(json: String) -> Result<MintVersion, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode MintVersion to JSON string
#[uniffi::export]
pub fn encode_mint_version(version: MintVersion) -> Result<String, FfiError> {
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<cdk::nuts::ContactInfo> for ContactInfo {
fn from(contact: cdk::nuts::ContactInfo) -> Self {
Self {
method: contact.method,
info: contact.info,
}
}
}
impl From<ContactInfo> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode ContactInfo from JSON string
#[uniffi::export]
pub fn decode_contact_info(json: String) -> Result<ContactInfo, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode ContactInfo to JSON string
#[uniffi::export]
pub fn encode_contact_info(info: ContactInfo) -> Result<String, FfiError> {
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<cdk::nuts::nut06::SupportedSettings> for SupportedSettings {
fn from(settings: cdk::nuts::nut06::SupportedSettings) -> Self {
Self {
supported: settings.supported,
}
}
}
impl From<SupportedSettings> 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<Amount>,
pub max_amount: Option<Amount>,
/// For bolt11, whether mint supports setting invoice description
pub description: Option<bool>,
}
impl From<cdk::nuts::nut04::MintMethodSettings> 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<MintMethodSettings> for cdk::nuts::nut04::MintMethodSettings {
type Error = FfiError;
fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
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<MintMethodSettings>,
pub disabled: bool,
}
impl From<cdk::nuts::nut04::Settings> 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<Nut04Settings> for cdk::nuts::nut04::Settings {
type Error = FfiError;
fn try_from(s: Nut04Settings) -> Result<Self, Self::Error> {
Ok(Self {
methods: s
.methods
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
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<Amount>,
pub max_amount: Option<Amount>,
/// For bolt11, whether mint supports amountless invoices
pub amountless: Option<bool>,
}
impl From<cdk::nuts::nut05::MeltMethodSettings> 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<MeltMethodSettings> for cdk::nuts::nut05::MeltMethodSettings {
type Error = FfiError;
fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
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<MeltMethodSettings>,
pub disabled: bool,
}
impl From<cdk::nuts::nut05::Settings> 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<Nut05Settings> for cdk::nuts::nut05::Settings {
type Error = FfiError;
fn try_from(s: Nut05Settings) -> Result<Self, Self::Error> {
Ok(Self {
methods: s
.methods
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
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<ProtectedEndpoint>,
}
/// 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<ProtectedEndpoint>,
}
impl From<cdk::nuts::ClearAuthSettings> 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<ClearAuthSettings> for cdk::nuts::ClearAuthSettings {
type Error = FfiError;
fn try_from(settings: ClearAuthSettings) -> Result<Self, Self::Error> {
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::<Result<Vec<_>, _>>()?,
})
}
}
impl From<cdk::nuts::BlindAuthSettings> 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<BlindAuthSettings> for cdk::nuts::BlindAuthSettings {
type Error = FfiError;
fn try_from(settings: BlindAuthSettings) -> Result<Self, Self::Error> {
Ok(Self {
bat_max_mint: settings.bat_max_mint,
protected_endpoints: settings
.protected_endpoints
.into_iter()
.map(|e| e.try_into())
.collect::<Result<Vec<_>, _>>()?,
})
}
}
impl From<cdk::nuts::ProtectedEndpoint> 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<ProtectedEndpoint> for cdk::nuts::ProtectedEndpoint {
type Error = FfiError;
fn try_from(endpoint: ProtectedEndpoint) -> Result<Self, Self::Error> {
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<ClearAuthSettings>,
/// NUT22 Settings - Blind authentication
pub nut22: Option<BlindAuthSettings>,
/// Supported currency units for minting
pub mint_units: Vec<CurrencyUnit>,
/// Supported currency units for melting
pub melt_units: Vec<CurrencyUnit>,
}
impl From<cdk::nuts::Nuts> 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<Nuts> for cdk::nuts::Nuts {
type Error = FfiError;
fn try_from(n: Nuts) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode Nuts from JSON string
#[uniffi::export]
pub fn decode_nuts(json: String) -> Result<Nuts, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode Nuts to JSON string
#[uniffi::export]
pub fn encode_nuts(nuts: Nuts) -> Result<String, FfiError> {
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<String>,
/// hex pubkey of the mint
pub pubkey: Option<String>,
/// implementation name and the version running
pub version: Option<MintVersion>,
/// short description of the mint
pub description: Option<String>,
/// long description
pub description_long: Option<String>,
/// Contact info
pub contact: Option<Vec<ContactInfo>>,
/// shows which NUTs the mint supports
pub nuts: Nuts,
/// Mint's icon URL
pub icon_url: Option<String>,
/// Mint's endpoint URLs
pub urls: Option<Vec<String>>,
/// message of the day that the wallet must display to the user
pub motd: Option<String>,
/// server unix timestamp
pub time: Option<u64>,
/// terms of url service of the mint
pub tos_url: Option<String>,
}
impl From<cdk::nuts::MintInfo> 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<MintInfo> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode MintInfo from JSON string
#[uniffi::export]
pub fn decode_mint_info(json: String) -> Result<MintInfo, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode MintInfo to JSON string
#[uniffi::export]
pub fn encode_mint_info(info: MintInfo) -> Result<String, FfiError> {
Ok(serde_json::to_string(&info)?)
}

View File

@@ -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::*;

View File

@@ -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<CdkState> 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<ProofState> 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<cdk::nuts::Proof> for Proof {
fn from(proof: cdk::nuts::Proof) -> Self {
Self { inner: proof }
}
}
impl From<Proof> 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<Witness> {
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<String>) -> bool {
use cdk::nuts::Id;
let ids: Vec<Id> = 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<String, FfiError> {
Ok(self.inner.y()?.to_string())
}
/// Get the DLEQ proof if present
pub fn dleq(&self) -> Option<ProofDleq> {
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<std::sync::Arc<Proof>>;
/// 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<cdk::nuts::ProofDleq> 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<ProofDleq> 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<cdk::nuts::BlindSignatureDleq> for BlindSignatureDleq {
fn from(dleq: cdk::nuts::BlindSignatureDleq) -> Self {
Self {
e: dleq.e.to_secret_hex(),
s: dleq.s.to_secret_hex(),
}
}
}
impl From<BlindSignatureDleq> 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<Amount, FfiError> {
let cdk_proofs: Vec<cdk::nuts::Proof> = 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<u64>,
/// Additional Public keys (as hex strings)
pub pubkeys: Vec<String>,
/// Refund keys (as hex strings)
pub refund_keys: Vec<String>,
/// Number of signatures required (default 1)
pub num_sigs: Option<u64>,
/// Signature flag (0 = SigInputs, 1 = SigAll)
pub sig_flag: u8,
/// Number of refund signatures required (default 1)
pub num_sigs_refund: Option<u64>,
}
impl From<cdk::nuts::nut11::Conditions> 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<Conditions> for cdk::nuts::nut11::Conditions {
type Error = FfiError;
fn try_from(conditions: Conditions) -> Result<Self, Self::Error> {
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::<Result<Vec<_>, _>>()?,
)
};
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::<Result<Vec<_>, _>>()?,
)
};
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode Conditions from JSON string
#[uniffi::export]
pub fn decode_conditions(json: String) -> Result<Conditions, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode Conditions to JSON string
#[uniffi::export]
pub fn encode_conditions(conditions: Conditions) -> Result<String, FfiError> {
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<String>,
},
/// HTLC Witness
HTLC {
/// Preimage
preimage: String,
/// Optional signatures
signatures: Option<Vec<String>>,
},
}
impl From<cdk::nuts::Witness> 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<Witness> 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<Conditions>,
},
/// HTLC (Hash Time Locked Contract) conditions
HTLC {
/// Hash of the preimage (as hex string)
hash: String,
/// Additional conditions
conditions: Option<Conditions>,
},
}
impl From<cdk::nuts::SpendingConditions> 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<SpendingConditions> for cdk::nuts::SpendingConditions {
type Error = FfiError;
fn try_from(spending_conditions: SpendingConditions) -> Result<Self, Self::Error> {
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<Proof>,
/// 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<SpendingConditions>,
/// Currency unit
pub unit: CurrencyUnit,
}
impl From<cdk::types::ProofInfo> 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<ProofInfo, FfiError> {
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<String, FfiError> {
// 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<String>,
}
impl From<cdk::nuts::nut07::ProofState> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode ProofStateUpdate from JSON string
#[uniffi::export]
pub fn decode_proof_state_update(json: String) -> Result<ProofStateUpdate, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode ProofStateUpdate to JSON string
#[uniffi::export]
pub fn encode_proof_state_update(update: ProofStateUpdate) -> Result<String, FfiError> {
Ok(serde_json::to_string(&update)?)
}

View File

@@ -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<Amount>,
/// 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<String>,
}
impl From<cdk::wallet::MintQuote> 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<MintQuote> for cdk::wallet::MintQuote {
type Error = FfiError;
fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode MintQuote from JSON string
#[uniffi::export]
pub fn decode_mint_quote(json: String) -> Result<MintQuote, FfiError> {
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<String, FfiError> {
Ok(serde_json::to_string(&quote)?)
}
/// 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<u64>,
/// Amount (optional)
pub amount: Option<Amount>,
/// Unit (optional)
pub unit: Option<CurrencyUnit>,
/// Pubkey (optional)
pub pubkey: Option<String>,
}
impl From<cdk::nuts::MintQuoteBolt11Response<String>> for MintQuoteBolt11Response {
fn from(response: cdk::nuts::MintQuoteBolt11Response<String>) -> 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<u64> {
self.expiry
}
/// Get amount
pub fn amount(&self) -> Option<Amount> {
self.amount
}
/// Get unit
pub fn unit(&self) -> Option<CurrencyUnit> {
self.unit.clone()
}
/// Get pubkey
pub fn pubkey(&self) -> Option<String> {
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<String>,
/// Request string (optional)
pub request: Option<String>,
/// Unit (optional)
pub unit: Option<CurrencyUnit>,
}
impl From<cdk::nuts::MeltQuoteBolt11Response<String>> for MeltQuoteBolt11Response {
fn from(response: cdk::nuts::MeltQuoteBolt11Response<String>) -> 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<String> {
self.payment_preimage.clone()
}
/// Get request
pub fn request(&self) -> Option<String> {
self.request.clone()
}
/// Get unit
pub fn unit(&self) -> Option<CurrencyUnit> {
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<cdk::nuts::PaymentMethod> 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<PaymentMethod> 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<String>,
/// Payment method
pub payment_method: PaymentMethod,
}
impl From<cdk::wallet::MeltQuote> 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<MeltQuote> for cdk::wallet::MeltQuote {
type Error = FfiError;
fn try_from(quote: MeltQuote) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode MeltQuote from JSON string
#[uniffi::export]
pub fn decode_melt_quote(json: String) -> Result<MeltQuote, FfiError> {
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<String, FfiError> {
Ok(serde_json::to_string(&quote)?)
}
/// FFI-compatible QuoteState
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
pub enum QuoteState {
Unpaid,
Paid,
Pending,
Issued,
}
impl From<cdk::nuts::nut05::QuoteState> 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<QuoteState> 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<cdk::nuts::MintQuoteState> 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<QuoteState> 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

View File

@@ -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<SubscriptionKind> 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<cdk::nuts::nut17::Kind> 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<String>,
/// Subscription ID (optional, will be generated if not provided)
pub id: Option<String>,
}
impl From<SubscribeParams> for cdk::nuts::nut17::Params<cdk::pub_sub::SubId> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode SubscribeParams from JSON string
#[uniffi::export]
pub fn decode_subscribe_params(json: String) -> Result<SubscribeParams, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode SubscribeParams to JSON string
#[uniffi::export]
pub fn encode_subscribe_params(params: SubscribeParams) -> Result<String, FfiError> {
Ok(serde_json::to_string(&params)?)
}
/// FFI-compatible ActiveSubscription
#[derive(uniffi::Object)]
pub struct ActiveSubscription {
inner: std::sync::Arc<tokio::sync::Mutex<cdk::wallet::subscription::ActiveSubscription>>,
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<NotificationPayload, FfiError> {
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<Option<NotificationPayload>, 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<ProofStateUpdate> },
/// Mint quote update
MintQuoteUpdate {
quote: std::sync::Arc<MintQuoteBolt11Response>,
},
/// Melt quote update
MeltQuoteUpdate {
quote: std::sync::Arc<MeltQuoteBolt11Response>,
},
}
impl From<cdk::nuts::NotificationPayload<String>> for NotificationPayload {
fn from(payload: cdk::nuts::NotificationPayload<String>) -> 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![],
}
}
}
}
}

View File

@@ -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<PublicKey>,
/// Unix timestamp
pub timestamp: u64,
/// Memo
pub memo: Option<String>,
/// User-defined metadata
pub metadata: HashMap<String, String>,
/// Quote ID if this is a mint or melt transaction
pub quote_id: Option<String>,
}
impl From<cdk::wallet::types::Transaction> 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<Transaction> for cdk::wallet::types::Transaction {
type Error = FfiError;
fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, _> =
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode Transaction from JSON string
#[uniffi::export]
pub fn decode_transaction(json: String) -> Result<Transaction, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode Transaction to JSON string
#[uniffi::export]
pub fn encode_transaction(transaction: Transaction) -> Result<String, FfiError> {
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<cdk::wallet::types::TransactionDirection> 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<TransactionDirection> 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<Self, FfiError> {
// 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<Self, FfiError> {
let cdk_proofs: Vec<cdk::nuts::Proof> = 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<cdk::wallet::types::TransactionId> for TransactionId {
fn from(id: cdk::wallet::types::TransactionId) -> Self {
Self {
hex: id.to_string(),
}
}
}
impl TryFrom<TransactionId> for cdk::wallet::types::TransactionId {
type Error = FfiError;
fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
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<cdk::nuts::AuthProof> 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<AuthProof> for cdk::nuts::AuthProof {
type Error = FfiError;
fn try_from(auth_proof: AuthProof) -> Result<Self, Self::Error> {
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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode AuthProof from JSON string
#[uniffi::export]
pub fn decode_auth_proof(json: String) -> Result<AuthProof, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode AuthProof to JSON string
#[uniffi::export]
pub fn encode_auth_proof(proof: AuthProof) -> Result<String, FfiError> {
Ok(serde_json::to_string(&proof)?)
}

View File

@@ -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<SendMemo> for cdk::wallet::SendMemo {
fn from(memo: SendMemo) -> Self {
cdk::wallet::SendMemo {
memo: memo.memo,
include_memo: memo.include_memo,
}
}
}
impl From<cdk::wallet::SendMemo> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode SendMemo from JSON string
#[uniffi::export]
pub fn decode_send_memo(json: String) -> Result<SendMemo, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode SendMemo to JSON string
#[uniffi::export]
pub fn encode_send_memo(memo: SendMemo) -> Result<String, FfiError> {
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<SendKind> 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<cdk::wallet::SendKind> 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<SendMemo>,
/// Spending conditions
pub conditions: Option<SpendingConditions>,
/// 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<u32>,
/// Metadata
pub metadata: HashMap<String, String>,
}
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<SendOptions> 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<cdk::wallet::SendOptions> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode SendOptions from JSON string
#[uniffi::export]
pub fn decode_send_options(json: String) -> Result<SendOptions, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode SendOptions to JSON string
#[uniffi::export]
pub fn encode_send_options(options: SendOptions) -> Result<String, FfiError> {
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<Self, FfiError> {
// 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<SecretKey> 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<cdk::nuts::SecretKey> 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<SecretKey>,
/// Preimages for HTLC conditions
pub preimages: Vec<String>,
/// Metadata
pub metadata: HashMap<String, String>,
}
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<ReceiveOptions> 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<cdk::wallet::ReceiveOptions> 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<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
/// Decode ReceiveOptions from JSON string
#[uniffi::export]
pub fn decode_receive_options(json: String) -> Result<ReceiveOptions, FfiError> {
Ok(serde_json::from_str(&json)?)
}
/// Encode ReceiveOptions to JSON string
#[uniffi::export]
pub fn encode_receive_options(options: ReceiveOptions) -> Result<String, FfiError> {
Ok(serde_json::to_string(&options)?)
}
/// FFI-compatible PreparedSend
#[derive(Debug, uniffi::Object)]
pub struct PreparedSend {
inner: Mutex<Option<cdk::wallet::PreparedSend>>,
id: String,
amount: Amount,
proofs: Proofs,
}
impl From<cdk::wallet::PreparedSend> 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<Self>,
memo: Option<String>,
) -> Result<Token, 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 {
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<Self>) -> 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<String>,
pub change: Option<Proofs>,
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<cdk::types::Melted> 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<MeltOptions> 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<cdk::nuts::MeltOptions> 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(),
},
}
}
}