use std::collections::HashMap;
use std::fmt;
use cdk_common::bitcoin;
use serde::{Deserialize, Serialize};
use super::amount::{Amount, SplitTarget};
use super::proof::{Proofs, SpendingConditions};
use crate::error::FfiError;
use crate::token::Token;
use crate::{CurrencyUnit, MintUrl, PublicKey};
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct SendMemo {
pub memo: String,
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 {
pub fn to_json(&self) -> Result<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
#[uniffi::export]
pub fn decode_send_memo(json: String) -> Result<SendMemo, FfiError> {
Ok(serde_json::from_str(&json)?)
}
#[uniffi::export]
pub fn encode_send_memo(memo: SendMemo) -> Result<String, FfiError> {
Ok(serde_json::to_string(&memo)?)
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
pub enum SendKind {
OnlineExact,
OnlineTolerance { tolerance: Amount },
OfflineExact,
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())
}
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct P2PKSigningKey {
pub pubkey: PublicKey,
pub derivation_path: String,
pub derivation_index: u32,
pub created_time: u64,
}
impl TryFrom<P2PKSigningKey> for cdk_common::wallet::P2PKSigningKey {
type Error = crate::error::FfiError;
fn try_from(key: P2PKSigningKey) -> Result<Self, FfiError> {
Ok(Self {
pubkey: key.pubkey.try_into()?,
derivation_path: key
.derivation_path
.parse()
.map_err(|e: bitcoin::bip32::Error| FfiError::Internal {
error_message: e.to_string(),
})?,
derivation_index: key.derivation_index,
created_time: key.created_time,
})
}
}
impl From<cdk_common::wallet::P2PKSigningKey> for P2PKSigningKey {
fn from(key: cdk_common::wallet::P2PKSigningKey) -> Self {
Self {
pubkey: key.pubkey.into(),
derivation_path: key.derivation_path.to_string(),
derivation_index: key.derivation_index,
created_time: key.created_time,
}
}
}
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(),
},
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum, Default,
)]
pub enum P2PKLockedProofSendMode {
#[default]
Swap,
SignAndSend,
}
impl From<P2PKLockedProofSendMode> for cdk::wallet::P2PKLockedProofSendMode {
fn from(mode: P2PKLockedProofSendMode) -> Self {
match mode {
P2PKLockedProofSendMode::Swap => cdk::wallet::P2PKLockedProofSendMode::Swap,
P2PKLockedProofSendMode::SignAndSend => {
cdk::wallet::P2PKLockedProofSendMode::SignAndSend
}
}
}
}
impl From<cdk::wallet::P2PKLockedProofSendMode> for P2PKLockedProofSendMode {
fn from(mode: cdk::wallet::P2PKLockedProofSendMode) -> Self {
match mode {
cdk::wallet::P2PKLockedProofSendMode::Swap => P2PKLockedProofSendMode::Swap,
cdk::wallet::P2PKLockedProofSendMode::SignAndSend => {
P2PKLockedProofSendMode::SignAndSend
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct SendOptions {
pub memo: Option<SendMemo>,
pub conditions: Option<SpendingConditions>,
pub amount_split_target: SplitTarget,
pub send_kind: SendKind,
pub include_fee: bool,
pub use_p2bk: bool,
pub max_proofs: Option<u32>,
pub metadata: HashMap<String, String>,
#[serde(default)]
pub p2pk_signing_keys: Vec<SecretKey>,
#[serde(default)]
pub p2pk_locked_proof_send_mode: P2PKLockedProofSendMode,
}
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(),
use_p2bk: false,
p2pk_signing_keys: Vec::new(),
p2pk_locked_proof_send_mode: P2PKLockedProofSendMode::Swap,
}
}
}
impl TryFrom<SendOptions> for cdk::wallet::SendOptions {
type Error = FfiError;
fn try_from(opts: SendOptions) -> Result<Self, Self::Error> {
let p2pk_signing_keys = opts
.p2pk_signing_keys
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?;
Ok(cdk::wallet::SendOptions {
memo: opts.memo.map(Into::into),
conditions: opts.conditions.map(TryInto::try_into).transpose()?,
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,
use_p2bk: opts.use_p2bk,
p2pk_signing_keys,
p2pk_locked_proof_send_mode: opts.p2pk_locked_proof_send_mode.into(),
})
}
}
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,
use_p2bk: opts.use_p2bk,
p2pk_signing_keys: opts.p2pk_signing_keys.into_iter().map(Into::into).collect(),
p2pk_locked_proof_send_mode: opts.p2pk_locked_proof_send_mode.into(),
}
}
}
impl SendOptions {
pub fn to_json(&self) -> Result<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
#[uniffi::export]
pub fn decode_send_options(json: String) -> Result<SendOptions, FfiError> {
Ok(serde_json::from_str(&json)?)
}
#[uniffi::export]
pub fn encode_send_options(options: SendOptions) -> Result<String, FfiError> {
Ok(serde_json::to_string(&options)?)
}
#[derive(Clone, Serialize, Deserialize, uniffi::Record)]
#[serde(transparent)]
pub struct SecretKey {
pub hex: String,
}
impl fmt::Debug for SecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretKey")
.field("hex", &"[redacted]")
.finish()
}
}
impl SecretKey {
pub fn from_hex(hex: String) -> Result<Self, FfiError> {
if hex.len() != 64 {
return Err(FfiError::internal(
"Secret key hex must be exactly 64 characters (32 bytes)",
));
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(FfiError::internal(
"Secret key hex contains invalid characters",
));
}
Ok(Self { hex })
}
pub fn random() -> Self {
use cdk::nuts::SecretKey as CdkSecretKey;
let secret_key = CdkSecretKey::generate();
Self {
hex: secret_key.to_secret_hex(),
}
}
}
impl TryFrom<SecretKey> for cdk::nuts::SecretKey {
type Error = FfiError;
fn try_from(key: SecretKey) -> Result<Self, Self::Error> {
cdk::nuts::SecretKey::from_hex(&key.hex)
.map_err(|e| FfiError::internal(format!("Invalid secret key: {}", e)))
}
}
impl From<cdk::nuts::SecretKey> for SecretKey {
fn from(key: cdk::nuts::SecretKey) -> Self {
Self {
hex: key.to_secret_hex(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct ReceiveOptions {
pub amount_split_target: SplitTarget,
#[serde(default)]
pub p2pk_signing_keys: Vec<SecretKey>,
pub preimages: Vec<String>,
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 TryFrom<ReceiveOptions> for cdk::wallet::ReceiveOptions {
type Error = FfiError;
fn try_from(opts: ReceiveOptions) -> Result<Self, Self::Error> {
let p2pk_signing_keys = opts
.p2pk_signing_keys
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?;
Ok(cdk::wallet::ReceiveOptions {
amount_split_target: opts.amount_split_target.into(),
p2pk_signing_keys,
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 {
pub fn to_json(&self) -> Result<String, FfiError> {
Ok(serde_json::to_string(self)?)
}
}
#[uniffi::export]
pub fn decode_receive_options(json: String) -> Result<ReceiveOptions, FfiError> {
Ok(serde_json::from_str(&json)?)
}
#[uniffi::export]
pub fn encode_receive_options(options: ReceiveOptions) -> Result<String, FfiError> {
Ok(serde_json::to_string(&options)?)
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
pub struct NUT13Options {
pub batch_size: u32,
pub max_gap: u32,
}
impl Default for NUT13Options {
fn default() -> Self {
cdk::wallet::NUT13Options::default().into()
}
}
impl TryFrom<NUT13Options> for cdk::wallet::NUT13Options {
type Error = FfiError;
fn try_from(opts: NUT13Options) -> Result<Self, Self::Error> {
Ok(cdk::wallet::NUT13Options::new(
opts.batch_size,
opts.max_gap,
)?)
}
}
impl From<cdk::wallet::NUT13Options> for NUT13Options {
fn from(opts: cdk::wallet::NUT13Options) -> Self {
NUT13Options {
batch_size: opts.batch_size,
max_gap: opts.max_gap,
}
}
}
#[derive(uniffi::Object)]
pub struct PreparedSend {
wallet: std::sync::Arc<cdk::Wallet>,
operation_id: uuid::Uuid,
amount: Amount,
options: cdk::wallet::SendOptions,
proofs_to_swap: cdk::nuts::Proofs,
proofs_to_send: cdk::nuts::Proofs,
swap_fee: Amount,
send_fee: Amount,
}
impl std::fmt::Debug for PreparedSend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedSend")
.field("operation_id", &self.operation_id)
.field("amount", &self.amount)
.finish()
}
}
impl PreparedSend {
pub fn new(
wallet: std::sync::Arc<cdk::Wallet>,
prepared: &cdk::wallet::PreparedSend<'_>,
) -> Self {
Self {
wallet,
operation_id: prepared.operation_id(),
amount: prepared.amount().into(),
options: prepared.options().clone(),
proofs_to_swap: prepared.proofs_to_swap().clone(),
proofs_to_send: prepared.proofs_to_send().clone(),
swap_fee: prepared.swap_fee().into(),
send_fee: prepared.send_fee().into(),
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl PreparedSend {
pub fn operation_id(&self) -> String {
self.operation_id.to_string()
}
pub fn amount(&self) -> Amount {
self.amount
}
pub fn proofs(&self) -> Proofs {
let mut all_proofs: Vec<_> = self
.proofs_to_swap
.iter()
.cloned()
.map(|p| p.into())
.collect();
all_proofs.extend(self.proofs_to_send.iter().cloned().map(|p| p.into()));
all_proofs
}
pub fn fee(&self) -> Amount {
Amount::new(self.swap_fee.value + self.send_fee.value)
}
pub async fn confirm(
self: std::sync::Arc<Self>,
memo: Option<String>,
) -> Result<Token, FfiError> {
let send_memo = memo.map(|m| cdk::wallet::SendMemo::for_token(&m));
let token = self
.wallet
.confirm_send(
self.operation_id,
self.amount.into(),
self.options.clone(),
self.proofs_to_swap.clone(),
self.proofs_to_send.clone(),
self.swap_fee.into(),
self.send_fee.into(),
send_memo,
)
.await?;
Ok(token.into())
}
pub async fn cancel(self: std::sync::Arc<Self>) -> Result<(), FfiError> {
self.wallet
.cancel_send(
self.operation_id,
self.proofs_to_swap.clone(),
self.proofs_to_send.clone(),
)
.await?;
Ok(())
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct FinalizedMelt {
pub quote_id: String,
pub state: super::quote::QuoteState,
pub preimage: Option<String>,
pub change: Option<Proofs>,
pub amount: Amount,
pub fee_paid: Amount,
}
impl From<cdk_common::common::FinalizedMelt> for FinalizedMelt {
fn from(finalized: cdk_common::common::FinalizedMelt) -> Self {
Self {
quote_id: finalized.quote_id().to_string(),
state: finalized.state().into(),
preimage: finalized.payment_proof().map(|s: &str| s.to_string()),
change: finalized
.change()
.map(|proofs| proofs.iter().cloned().map(|p| p.into()).collect()),
amount: finalized.amount().into(),
fee_paid: finalized.fee_paid().into(),
}
}
}
#[derive(uniffi::Object)]
pub struct PreparedMelt {
wallet: std::sync::Arc<cdk::Wallet>,
operation_id: uuid::Uuid,
quote: cdk_common::wallet::MeltQuote,
proofs: cdk::nuts::Proofs,
proofs_to_swap: cdk::nuts::Proofs,
swap_fee: Amount,
input_fee: Amount,
input_fee_without_swap: Amount,
metadata: HashMap<String, String>,
}
impl std::fmt::Debug for PreparedMelt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedMelt")
.field("operation_id", &self.operation_id)
.field("quote_id", &self.quote.id)
.field("amount", &self.quote.amount)
.finish()
}
}
impl PreparedMelt {
pub fn new(
wallet: std::sync::Arc<cdk::Wallet>,
prepared: &cdk::wallet::PreparedMelt<'_>,
) -> Self {
Self {
wallet,
operation_id: prepared.operation_id(),
quote: prepared.quote().clone(),
proofs: prepared.proofs().clone(),
proofs_to_swap: prepared.proofs_to_swap().clone(),
swap_fee: prepared.swap_fee().into(),
input_fee: prepared.input_fee().into(),
input_fee_without_swap: prepared.input_fee_without_swap().into(),
metadata: HashMap::new(),
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl PreparedMelt {
pub fn operation_id(&self) -> String {
self.operation_id.to_string()
}
pub fn quote_id(&self) -> String {
self.quote.id.clone()
}
pub fn amount(&self) -> Amount {
self.quote.amount.into()
}
pub fn fee_reserve(&self) -> Amount {
self.quote.fee_reserve.into()
}
pub fn swap_fee(&self) -> Amount {
self.swap_fee
}
pub fn input_fee(&self) -> Amount {
self.input_fee
}
pub fn total_fee(&self) -> Amount {
Amount::new(self.swap_fee.value + self.input_fee.value)
}
pub fn requires_swap(&self) -> bool {
!self.proofs_to_swap.is_empty()
}
pub fn total_fee_with_swap(&self) -> Amount {
Amount::new(self.swap_fee.value + self.input_fee.value)
}
pub fn input_fee_without_swap(&self) -> Amount {
self.input_fee_without_swap
}
pub fn fee_savings_without_swap(&self) -> Amount {
let total_with = self.swap_fee.value + self.input_fee.value;
let total_without = self.input_fee_without_swap.value;
if total_with > total_without {
Amount::new(total_with - total_without)
} else {
Amount::new(0)
}
}
pub fn change_amount_without_swap(&self) -> Amount {
use cdk::nuts::nut00::ProofsMethods;
let all_proofs_total = self.proofs.total_amount().unwrap_or(cdk::Amount::ZERO)
+ self
.proofs_to_swap
.total_amount()
.unwrap_or(cdk::Amount::ZERO);
let needed =
self.quote.amount + self.quote.fee_reserve + self.input_fee_without_swap.into();
all_proofs_total
.checked_sub(needed)
.map(|a| a.into())
.unwrap_or(Amount::new(0))
}
pub fn proofs(&self) -> Proofs {
self.proofs.iter().cloned().map(|p| p.into()).collect()
}
pub async fn confirm(&self) -> Result<FinalizedMelt, FfiError> {
self.confirm_with_options(MeltConfirmOptions::default())
.await
}
pub async fn confirm_with_options(
&self,
options: MeltConfirmOptions,
) -> Result<FinalizedMelt, FfiError> {
let finalized = self
.wallet
.confirm_prepared_melt_with_options(
self.operation_id,
self.quote.clone(),
self.proofs.clone(),
self.proofs_to_swap.clone(),
self.input_fee.into(),
self.input_fee_without_swap.into(),
self.metadata.clone(),
options.into(),
)
.await?;
Ok(finalized.into())
}
pub async fn cancel(&self) -> Result<(), FfiError> {
self.wallet
.cancel_prepared_melt(
self.operation_id,
self.proofs.clone(),
self.proofs_to_swap.clone(),
)
.await?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
pub enum MeltOptions {
Mpp { amount: Amount },
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(),
},
}
}
}
#[derive(Debug, Clone, uniffi::Record)]
pub struct Restored {
pub spent: Amount,
pub unspent: Amount,
pub pending: Amount,
}
impl From<cdk_common::wallet::Restored> for Restored {
fn from(restored: cdk_common::wallet::Restored) -> Self {
Self {
spent: restored.spent.into(),
unspent: restored.unspent.into(),
pending: restored.pending.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, uniffi::Record)]
pub struct RecoveryReport {
pub recovered: u64,
pub compensated: u64,
pub skipped: u64,
pub failed: u64,
}
impl From<cdk::wallet::RecoveryReport> for RecoveryReport {
fn from(report: cdk::wallet::RecoveryReport) -> Self {
Self {
recovered: report.recovered as u64,
compensated: report.compensated as u64,
skipped: report.skipped as u64,
failed: report.failed as u64,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, uniffi::Record)]
pub struct MeltConfirmOptions {
pub skip_swap: bool,
}
impl From<MeltConfirmOptions> for cdk::wallet::MeltConfirmOptions {
fn from(opts: MeltConfirmOptions) -> Self {
cdk::wallet::MeltConfirmOptions {
skip_swap: opts.skip_swap,
}
}
}
impl From<cdk::wallet::MeltConfirmOptions> for MeltConfirmOptions {
fn from(opts: cdk::wallet::MeltConfirmOptions) -> Self {
Self {
skip_swap: opts.skip_swap,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
pub struct WalletKey {
pub mint_url: MintUrl,
pub unit: CurrencyUnit,
}
impl TryFrom<WalletKey> for cdk::WalletKey {
type Error = FfiError;
fn try_from(value: WalletKey) -> Result<Self, Self::Error> {
Ok(Self {
mint_url: value.mint_url.try_into()?,
unit: value.unit.into(),
})
}
}
impl From<cdk::WalletKey> for WalletKey {
fn from(value: cdk::WalletKey) -> Self {
Self {
mint_url: value.mint_url.into(),
unit: value.unit.into(),
}
}
}