Cesar Rodas 3 týždňov pred
rodič
commit
bf93ed05b4

+ 3 - 0
.gitignore

@@ -29,5 +29,8 @@ __pycache__
 libcdk_ffi*
 cdk_ffi.py
 
+# WASM build output
+crates/cdk-wasm/pkg/
+
 # llm artifacts
 AGENTS.md

+ 34 - 2
Cargo.lock

@@ -1210,8 +1210,6 @@ dependencies = [
  "lightning-invoice 0.34.0",
  "nostr-sdk",
  "rand 0.9.2",
- "regex",
- "ring 0.17.14",
  "rustls 0.23.36",
  "serde",
  "serde_json",
@@ -1751,6 +1749,29 @@ dependencies = [
 ]
 
 [[package]]
+name = "cdk-wasm"
+version = "0.15.0-rc.1"
+dependencies = [
+ "async-trait",
+ "bip39",
+ "cdk",
+ "cdk-common",
+ "getrandom 0.2.17",
+ "js-sys",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "url",
+ "uuid",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test",
+ "web-sys",
+]
+
+[[package]]
 name = "cfg-if"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6598,6 +6619,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "serde_core"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 1 - 1
Cargo.toml

@@ -4,6 +4,7 @@ members = [
 ]
 exclude = [
     "fuzz",
+    "crates/.claude",
 ]
 resolver = "2"
 
@@ -108,7 +109,6 @@ reqwest = { version = "0.12", default-features = false, features = [
 once_cell = "1.20.2"
 web-time = "1.1.0"
 rand = "0.9.1"
-regex = "1"
 home = "0.5.5"
 tonic = { version = "0.14.2", default-features = false }
 tonic-prost = "0.14.2"

+ 1 - 1
crates/cdk-http-client/Cargo.toml

@@ -18,7 +18,7 @@ url.workspace = true
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 reqwest = { workspace = true }
-regex = { workspace = true }
+regex = "1"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 reqwest = { version = "0.12", default-features = false, features = ["json"] }

+ 37 - 0
crates/cdk-wasm/Cargo.toml

@@ -0,0 +1,37 @@
+[package]
+name = "cdk-wasm"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+description = "WASM bindings for the CDK wallet"
+homepage = "https://github.com/cashubtc/cdk"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+name = "cdk_wasm"
+
+[dependencies]
+async-trait = { workspace = true }
+bip39 = { workspace = true }
+cdk = { workspace = true, default-features = false, features = ["wallet"] }
+cdk-common.workspace = true
+serde = { workspace = true, features = ["derive", "rc"] }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true }
+url = { workspace = true }
+uuid = { workspace = true, features = ["v4"] }
+wasm-bindgen = "0.2"
+wasm-bindgen-futures = "0.4"
+serde-wasm-bindgen = "0.6"
+js-sys = "0.3"
+web-sys = { version = "0.3", features = ["console"] }
+getrandom = { version = "0.2", features = ["js"] }
+
+[dev-dependencies]
+wasm-bindgen-test = "0.3"
+
+[lints]
+workspace = true

+ 38 - 0
crates/cdk-wasm/js/storage.js

@@ -0,0 +1,38 @@
+// Default storage backend using localStorage with "cdk:" prefix
+let backend = {
+    get(key) {
+        try {
+            return localStorage.getItem("cdk:" + key);
+        } catch (_) {
+            return null;
+        }
+    },
+    set(key, value) {
+        try {
+            localStorage.setItem("cdk:" + key, value);
+        } catch (_) {}
+    },
+    remove(key) {
+        try {
+            localStorage.removeItem("cdk:" + key);
+        } catch (_) {}
+    },
+};
+
+export function storageGet(key) {
+    return backend.get(key);
+}
+
+export function storageSet(key, value) {
+    backend.set(key, value);
+}
+
+export function storageRemove(key) {
+    backend.remove(key);
+}
+
+// Swap the default backend at runtime.
+// `newBackend` must implement { get(key)->string|null, set(key,value), remove(key) }
+export function setStorageBackend(newBackend) {
+    backend = newBackend;
+}

+ 917 - 0
crates/cdk-wasm/src/database.rs

@@ -0,0 +1,917 @@
+//! WASM Database bindings
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cdk_common::database::WalletDatabase as CdkWalletDatabase;
+use cdk_common::wallet::WalletSaga;
+
+use crate::error::WasmError;
+use crate::types::*;
+
+/// WASM-compatible wallet database trait with all read and write operations
+/// This trait mirrors the CDK WalletDatabase trait structure
+#[async_trait::async_trait(?Send)]
+pub trait WalletDatabase: 'static {
+    // ========== Read methods ==========
+
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, WasmError>;
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, WasmError>;
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, WasmError>;
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, WasmError>;
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, WasmError>;
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, WasmError>;
+    async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, WasmError>;
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, WasmError>;
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, WasmError>;
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, WasmError>;
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, WasmError>;
+    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, WasmError>;
+    async fn get_balance(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+    ) -> Result<u64, WasmError>;
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, WasmError>;
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, WasmError>;
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, WasmError>;
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, WasmError>;
+    async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), WasmError>;
+    async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<(), WasmError>;
+
+    // ========== Write methods ==========
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), WasmError>;
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), WasmError>;
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), WasmError>;
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), WasmError>;
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), WasmError>;
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, WasmError>;
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), WasmError>;
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), WasmError>;
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), WasmError>;
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), WasmError>;
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), WasmError>;
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), WasmError>;
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), WasmError>;
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), WasmError>;
+    async fn remove_keys(&self, id: Id) -> Result<(), WasmError>;
+
+    // ========== Saga management methods ==========
+
+    async fn add_saga(&self, saga_json: String) -> Result<(), WasmError>;
+    async fn get_saga(&self, id: String) -> Result<Option<String>, WasmError>;
+    async fn update_saga(&self, saga_json: String) -> Result<bool, WasmError>;
+    async fn delete_saga(&self, id: String) -> Result<(), WasmError>;
+    async fn get_incomplete_sagas(&self) -> Result<Vec<String>, WasmError>;
+
+    // ========== Proof reservation methods ==========
+
+    async fn reserve_proofs(
+        &self,
+        ys: Vec<PublicKey>,
+        operation_id: String,
+    ) -> Result<(), WasmError>;
+    async fn release_proofs(&self, operation_id: String) -> Result<(), WasmError>;
+    async fn get_reserved_proofs(&self, operation_id: String) -> Result<Vec<ProofInfo>, WasmError>;
+
+    // ========== Quote reservation methods ==========
+
+    async fn reserve_melt_quote(
+        &self,
+        quote_id: String,
+        operation_id: String,
+    ) -> Result<(), WasmError>;
+    async fn release_melt_quote(&self, operation_id: String) -> Result<(), WasmError>;
+    async fn reserve_mint_quote(
+        &self,
+        quote_id: String,
+        operation_id: String,
+    ) -> Result<(), WasmError>;
+    async fn release_mint_quote(&self, operation_id: String) -> Result<(), WasmError>;
+}
+
+/// Internal bridge to convert from the WASM trait to the CDK database trait
+pub(crate) struct WalletDatabaseBridge {
+    db: Arc<dyn WalletDatabase>,
+}
+
+impl WalletDatabaseBridge {
+    pub fn new(db: Arc<dyn WalletDatabase>) -> Self {
+        Self { db }
+    }
+}
+
+impl std::fmt::Debug for WalletDatabaseBridge {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "WalletDatabaseBridge")
+    }
+}
+
+#[async_trait::async_trait(?Send)]
+impl CdkWalletDatabase<cdk::cdk_database::Error> for WalletDatabaseBridge {
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, cdk::cdk_database::Error> {
+        self.db
+            .kv_read(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, cdk::cdk_database::Error> {
+        self.db
+            .kv_list(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_mint(
+        &self,
+        mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<Option<cdk::nuts::MintInfo>, cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.into();
+        let result = self
+            .db
+            .get_mint(wasm_mint_url)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mints(
+        &self,
+    ) -> Result<
+        HashMap<cdk::mint_url::MintUrl, Option<cdk::nuts::MintInfo>>,
+        cdk::cdk_database::Error,
+    > {
+        let result = self
+            .db
+            .get_mints()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        let mut cdk_result = HashMap::new();
+        for (wasm_mint_url, mint_info_opt) in result {
+            let cdk_url = wasm_mint_url
+                .try_into()
+                .map_err(|e: WasmError| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+            cdk_result.insert(cdk_url, mint_info_opt.map(Into::into));
+        }
+        Ok(cdk_result)
+    }
+
+    async fn get_mint_keysets(
+        &self,
+        mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<Option<Vec<cdk::nuts::KeySetInfo>>, cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.into();
+        let result = self
+            .db
+            .get_mint_keysets(wasm_mint_url)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+    }
+
+    async fn get_keyset_by_id(
+        &self,
+        keyset_id: &cdk::nuts::Id,
+    ) -> Result<Option<cdk::nuts::KeySetInfo>, cdk::cdk_database::Error> {
+        let wasm_id = (*keyset_id).into();
+        let result = self
+            .db
+            .get_keyset_by_id(wasm_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mint_quote(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .transpose()?)
+    }
+
+    async fn get_mint_quotes(
+        &self,
+    ) -> Result<Vec<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_mint_quotes()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .into_iter()
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn get_unissued_mint_quotes(
+        &self,
+    ) -> Result<Vec<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_unissued_mint_quotes()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .into_iter()
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn get_melt_quote(
+        &self,
+        quote_id: &str,
+    ) -> Result<Option<cdk::wallet::MeltQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .transpose()?)
+    }
+
+    async fn get_melt_quotes(
+        &self,
+    ) -> Result<Vec<cdk::wallet::MeltQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_melt_quotes()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .into_iter()
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn get_keys(
+        &self,
+        id: &cdk::nuts::Id,
+    ) -> Result<Option<cdk::nuts::Keys>, cdk::cdk_database::Error> {
+        let wasm_id: Id = (*id).into();
+        let result = self
+            .db
+            .get_keys(wasm_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        result
+            .map(|keys| {
+                keys.try_into()
+                    .map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })
+            })
+            .transpose()
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<cdk::mint_url::MintUrl>,
+        unit: Option<cdk::nuts::CurrencyUnit>,
+        state: Option<Vec<cdk::nuts::State>>,
+        spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>>,
+    ) -> Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.map(Into::into);
+        let wasm_unit = unit.map(Into::into);
+        let wasm_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let wasm_spending_conditions =
+            spending_conditions.map(|sc| sc.into_iter().map(Into::into).collect());
+
+        let result = self
+            .db
+            .get_proofs(wasm_mint_url, wasm_unit, wasm_state, wasm_spending_conditions)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        let cdk_result: Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> = result
+            .into_iter()
+            .map(|info| {
+                Ok(cdk::types::ProofInfo {
+                    proof: info.proof.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    y: info.y.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    mint_url: info.mint_url.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()
+                        .map_err(|e: WasmError| {
+                            cdk::cdk_database::Error::Database(e.to_string().into())
+                        })?,
+                    unit: info.unit.into(),
+                    used_by_operation: info
+                        .used_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                    created_by_operation: info
+                        .created_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                })
+            })
+            .collect();
+
+        cdk_result
+    }
+
+    async fn get_proofs_by_ys(
+        &self,
+        ys: Vec<cdk::nuts::PublicKey>,
+    ) -> Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> {
+        let wasm_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
+
+        let result = self
+            .db
+            .get_proofs_by_ys(wasm_ys)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        let cdk_result: Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> = result
+            .into_iter()
+            .map(|info| {
+                Ok(cdk::types::ProofInfo {
+                    proof: info.proof.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    y: info.y.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    mint_url: info.mint_url.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()
+                        .map_err(|e: WasmError| {
+                            cdk::cdk_database::Error::Database(e.to_string().into())
+                        })?,
+                    unit: info.unit.into(),
+                    used_by_operation: info
+                        .used_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                    created_by_operation: info
+                        .created_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                })
+            })
+            .collect();
+
+        cdk_result
+    }
+
+    async fn get_balance(
+        &self,
+        mint_url: Option<cdk::mint_url::MintUrl>,
+        unit: Option<cdk::nuts::CurrencyUnit>,
+        state: Option<Vec<cdk::nuts::State>>,
+    ) -> Result<u64, cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.map(Into::into);
+        let wasm_unit = unit.map(Into::into);
+        let wasm_state = state.map(|s| s.into_iter().map(Into::into).collect());
+
+        self.db
+            .get_balance(wasm_mint_url, wasm_unit, wasm_state)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_transaction(
+        &self,
+        transaction_id: cdk::wallet::types::TransactionId,
+    ) -> Result<Option<cdk::wallet::types::Transaction>, cdk::cdk_database::Error> {
+        let wasm_id = transaction_id.into();
+        let result = self
+            .db
+            .get_transaction(wasm_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        result
+            .map(|tx| tx.try_into())
+            .transpose()
+            .map_err(|e: WasmError| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn list_transactions(
+        &self,
+        mint_url: Option<cdk::mint_url::MintUrl>,
+        direction: Option<cdk::wallet::types::TransactionDirection>,
+        unit: Option<cdk::nuts::CurrencyUnit>,
+    ) -> Result<Vec<cdk::wallet::types::Transaction>, cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.map(Into::into);
+        let wasm_direction = direction.map(Into::into);
+        let wasm_unit = unit.map(Into::into);
+
+        let result = self
+            .db
+            .list_transactions(wasm_mint_url, wasm_direction, wasm_unit)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        result
+            .into_iter()
+            .map(|tx| tx.try_into())
+            .collect::<Result<Vec<_>, WasmError>>()
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_proofs(
+        &self,
+        added: Vec<cdk::types::ProofInfo>,
+        removed_ys: Vec<cdk::nuts::PublicKey>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_added: Vec<ProofInfo> = added.into_iter().map(Into::into).collect();
+        let wasm_removed_ys: Vec<PublicKey> = removed_ys.into_iter().map(Into::into).collect();
+        self.db
+            .update_proofs(wasm_added, wasm_removed_ys)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<cdk::nuts::PublicKey>,
+        state: cdk::nuts::State,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
+        let wasm_state = state.into();
+        self.db
+            .update_proofs_state(wasm_ys, wasm_state)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_transaction(
+        &self,
+        transaction: cdk::wallet::types::Transaction,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_transaction = transaction.into();
+        self.db
+            .add_transaction(wasm_transaction)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: cdk::mint_url::MintUrl,
+        new_mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_old = old_mint_url.into();
+        let wasm_new = new_mint_url.into();
+        self.db
+            .update_mint_url(wasm_old, wasm_new)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn increment_keyset_counter(
+        &self,
+        keyset_id: &cdk::nuts::Id,
+        count: u32,
+    ) -> Result<u32, cdk::cdk_database::Error> {
+        let wasm_id = (*keyset_id).into();
+        self.db
+            .increment_keyset_counter(wasm_id, count)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_mint(
+        &self,
+        mint_url: cdk::mint_url::MintUrl,
+        mint_info: Option<cdk::nuts::MintInfo>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.into();
+        let wasm_mint_info = mint_info.map(Into::into);
+        self.db
+            .add_mint(wasm_mint_url, wasm_mint_info)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_mint(
+        &self,
+        mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.into();
+        self.db
+            .remove_mint(wasm_mint_url)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: cdk::mint_url::MintUrl,
+        keysets: Vec<cdk::nuts::KeySetInfo>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_mint_url = mint_url.into();
+        let wasm_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
+        self.db
+            .add_mint_keysets(wasm_mint_url, wasm_keysets)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_mint_quote(
+        &self,
+        quote: cdk::wallet::MintQuote,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_quote = quote.into();
+        self.db
+            .add_mint_quote(wasm_quote)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .remove_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_melt_quote(
+        &self,
+        quote: cdk::wallet::MeltQuote,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_quote = quote.into();
+        self.db
+            .add_melt_quote(wasm_quote)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .remove_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_keys(&self, keyset: cdk::nuts::KeySet) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_keyset: KeySet = keyset.into();
+        self.db
+            .add_keys(wasm_keyset)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_keys(&self, id: &cdk::nuts::Id) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_id = (*id).into();
+        self.db
+            .remove_keys(wasm_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_transaction(
+        &self,
+        transaction_id: cdk::wallet::types::TransactionId,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_id = transaction_id.into();
+        self.db
+            .remove_transaction(wasm_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_saga(&self, saga: WalletSaga) -> Result<(), cdk::cdk_database::Error> {
+        let json = serde_json::to_string(&saga)
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        self.db
+            .add_saga(json)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_saga(
+        &self,
+        id: &uuid::Uuid,
+    ) -> Result<Option<WalletSaga>, cdk::cdk_database::Error> {
+        let json_opt = self
+            .db
+            .get_saga(id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        match json_opt {
+            Some(json) => {
+                let saga: WalletSaga = serde_json::from_str(&json)
+                    .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+                Ok(Some(saga))
+            }
+            None => Ok(None),
+        }
+    }
+
+    async fn update_saga(&self, saga: WalletSaga) -> Result<bool, cdk::cdk_database::Error> {
+        let json = serde_json::to_string(&saga)
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        self.db
+            .update_saga(json)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .delete_saga(id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_incomplete_sagas(&self) -> Result<Vec<WalletSaga>, cdk::cdk_database::Error> {
+        let json_vec = self
+            .db
+            .get_incomplete_sagas()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        json_vec
+            .into_iter()
+            .map(|json| {
+                serde_json::from_str(&json)
+                    .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+            })
+            .collect()
+    }
+
+    async fn reserve_proofs(
+        &self,
+        ys: Vec<cdk::nuts::PublicKey>,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let wasm_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
+        self.db
+            .reserve_proofs(wasm_ys, operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn release_proofs(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .release_proofs(operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn get_reserved_proofs(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> {
+        let result = self
+            .db
+            .get_reserved_proofs(operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        result
+            .into_iter()
+            .map(|info| {
+                Ok(cdk::types::ProofInfo {
+                    proof: info.proof.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    y: info.y.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    mint_url: info.mint_url.try_into().map_err(|e: WasmError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()
+                        .map_err(|e: WasmError| {
+                            cdk::cdk_database::Error::Database(e.to_string().into())
+                        })?,
+                    unit: info.unit.into(),
+                    used_by_operation: info
+                        .used_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                    created_by_operation: info
+                        .created_by_operation
+                        .map(|id| uuid::Uuid::parse_str(&id))
+                        .transpose()
+                        .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?,
+                })
+            })
+            .collect()
+    }
+
+    async fn reserve_melt_quote(
+        &self,
+        quote_id: &str,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .reserve_melt_quote(quote_id.to_string(), operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn release_melt_quote(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .release_melt_quote(operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn reserve_mint_quote(
+        &self,
+        quote_id: &str,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .reserve_mint_quote(quote_id.to_string(), operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn release_mint_quote(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .release_mint_quote(operation_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_write(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .kv_write(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+                value.to_vec(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_remove(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.db
+            .kv_remove(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+}
+
+/// Helper function to create a CDK database from the WASM trait
+pub fn create_cdk_database_from_wasm(
+    db: Arc<dyn WalletDatabase>,
+) -> Arc<dyn CdkWalletDatabase<cdk::cdk_database::Error>> {
+    Arc::new(WalletDatabaseBridge::new(db))
+}

+ 73 - 0
crates/cdk-wasm/src/error.rs

@@ -0,0 +1,73 @@
+//! WASM Error types
+
+use cdk::Error as CdkError;
+use cdk_common::error::ErrorResponse;
+use wasm_bindgen::prelude::*;
+
+/// WASM Error type that wraps CDK errors for cross-language use
+#[derive(Debug, thiserror::Error)]
+pub enum WasmError {
+    /// CDK error with protocol-compliant error code
+    #[error("[{code}] {error_message}")]
+    Cdk {
+        code: u32,
+        error_message: String,
+    },
+
+    /// Internal/infrastructure error
+    #[error("{error_message}")]
+    Internal {
+        error_message: String,
+    },
+}
+
+impl WasmError {
+    /// Create an internal error from any type that implements ToString
+    pub fn internal(msg: impl ToString) -> Self {
+        WasmError::Internal {
+            error_message: msg.to_string(),
+        }
+    }
+
+    /// Create a database error (uses Unknown code 50000)
+    pub fn database(msg: impl ToString) -> Self {
+        WasmError::Cdk {
+            code: 50000,
+            error_message: msg.to_string(),
+        }
+    }
+}
+
+impl From<CdkError> for WasmError {
+    fn from(err: CdkError) -> Self {
+        let response = ErrorResponse::from(err);
+        WasmError::Cdk {
+            code: response.code.to_code() as u32,
+            error_message: response.detail,
+        }
+    }
+}
+
+impl From<cdk::amount::Error> for WasmError {
+    fn from(err: cdk::amount::Error) -> Self {
+        WasmError::internal(err)
+    }
+}
+
+impl From<cdk::nuts::nut00::Error> for WasmError {
+    fn from(err: cdk::nuts::nut00::Error) -> Self {
+        WasmError::internal(err)
+    }
+}
+
+impl From<serde_json::Error> for WasmError {
+    fn from(err: serde_json::Error) -> Self {
+        WasmError::internal(err)
+    }
+}
+
+impl From<WasmError> for JsValue {
+    fn from(err: WasmError) -> Self {
+        JsValue::from_str(&err.to_string())
+    }
+}

+ 25 - 0
crates/cdk-wasm/src/lib.rs

@@ -0,0 +1,25 @@
+//! CDK WASM bindings
+//!
+//! Web Assembly bindings for the Cashu Development Kit wallet.
+//! Provides JavaScript/TypeScript access to the CDK wallet functionality.
+
+#![allow(missing_docs)]
+
+#[cfg(not(target_arch = "wasm32"))]
+compile_error!("cdk-wasm only supports wasm32 targets");
+
+#[cfg(target_arch = "wasm32")]
+#[path = ""]
+mod wasm_modules {
+    pub mod database;
+    pub mod error;
+    pub mod local_storage;
+    pub mod logging;
+    pub mod token;
+    pub mod types;
+    pub mod wallet;
+    pub mod wallet_repository;
+}
+
+#[cfg(target_arch = "wasm32")]
+pub use wasm_modules::*;

+ 606 - 0
crates/cdk-wasm/src/local_storage.rs

@@ -0,0 +1,606 @@
+//! LocalStorage-backed wallet database for WASM
+//!
+//! A write-through cache implementation: all data lives in memory (HashMaps/Vecs)
+//! for fast reads, and every write is persisted to a JS storage backend
+//! (localStorage by default). On construction the cache is hydrated from storage.
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cdk_common::database::{Error, WalletDatabase};
+use cdk_common::mint_url::MintUrl;
+use cdk_common::nuts::{
+    CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
+};
+use cdk_common::wallet::{
+    MeltQuote, MintQuote, ProofInfo, Transaction, TransactionDirection, TransactionId, WalletSaga,
+};
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use tokio::sync::RwLock;
+use wasm_bindgen::prelude::*;
+
+// ---------------------------------------------------------------------------
+// JS FFI – imported from js/storage.js
+// ---------------------------------------------------------------------------
+#[wasm_bindgen(module = "/js/storage.js")]
+extern "C" {
+    #[wasm_bindgen(js_name = "storageGet")]
+    fn storage_get(key: &str) -> Option<String>;
+
+    #[wasm_bindgen(js_name = "storageSet")]
+    fn storage_set(key: &str, value: &str);
+
+    #[wasm_bindgen(js_name = "storageRemove")]
+    fn storage_remove(key: &str);
+
+    #[wasm_bindgen(js_name = "setStorageBackend")]
+    pub fn set_storage_backend(backend: JsValue);
+}
+
+// ---------------------------------------------------------------------------
+// Re-export setStorageBackend to JS consumers of the WASM module
+// ---------------------------------------------------------------------------
+#[wasm_bindgen(js_name = "setStorageBackend")]
+pub fn set_storage_backend_wasm(backend: JsValue) {
+    set_storage_backend(backend);
+}
+
+// ---------------------------------------------------------------------------
+// Storage keys (one JSON blob per "table")
+// ---------------------------------------------------------------------------
+const KEY_MINTS: &str = "mints";
+const KEY_MINT_KEYSETS: &str = "mint_keysets";
+const KEY_KEYSET_COUNTER: &str = "keyset_counter";
+const KEY_KEYS: &str = "keys";
+const KEY_PROOFS: &str = "proofs";
+const KEY_MINT_QUOTES: &str = "mint_quotes";
+const KEY_MELT_QUOTES: &str = "melt_quotes";
+const KEY_TRANSACTIONS: &str = "transactions";
+const KEY_SAGAS: &str = "sagas";
+const KEY_PROOF_RESERVATIONS: &str = "proof_reservations";
+const KEY_MELT_QUOTE_RESERVATIONS: &str = "melt_quote_reservations";
+const KEY_MINT_QUOTE_RESERVATIONS: &str = "mint_quote_reservations";
+const KEY_KV: &str = "kv";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+fn load_or_default<T: DeserializeOwned + Default>(key: &str) -> T {
+    storage_get(key)
+        .and_then(|json| serde_json::from_str(&json).ok())
+        .unwrap_or_default()
+}
+
+fn persist<T: Serialize>(key: &str, value: &T) {
+    if let Ok(json) = serde_json::to_string(value) {
+        storage_set(key, &json);
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Inner state – same shape as the old MemoryDatabase
+// ---------------------------------------------------------------------------
+// KV uses a single String key (components joined by \0) because JSON object
+// keys must be strings — tuple keys like (String, String, String) cannot
+// round-trip through serde_json as map keys.
+
+type Db = Arc<dyn WalletDatabase<Error> + Send + Sync>;
+
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+struct Inner {
+    mints: HashMap<MintUrl, Option<MintInfo>>,
+    mint_keysets: HashMap<MintUrl, Vec<KeySetInfo>>,
+    keyset_counter: HashMap<Id, u32>,
+    keys: HashMap<Id, Keys>,
+    proofs: Vec<ProofInfo>,
+    mint_quotes: Vec<MintQuote>,
+    melt_quotes: Vec<MeltQuote>,
+    transactions: Vec<Transaction>,
+    sagas: HashMap<String, WalletSaga>,
+    proof_reservations: HashMap<uuid::Uuid, Vec<PublicKey>>,
+    melt_quote_reservations: HashMap<uuid::Uuid, String>,
+    mint_quote_reservations: HashMap<uuid::Uuid, String>,
+    /// Composite key: "primary\0secondary\0key" → bytes (hex-encoded for JSON)
+    kv: HashMap<String, Vec<u8>>,
+}
+
+impl Inner {
+    /// Hydrate from JS storage, falling back to empty defaults.
+    fn load() -> Self {
+        Self {
+            mints: load_or_default(KEY_MINTS),
+            mint_keysets: load_or_default(KEY_MINT_KEYSETS),
+            keyset_counter: load_or_default(KEY_KEYSET_COUNTER),
+            keys: load_or_default(KEY_KEYS),
+            proofs: load_or_default(KEY_PROOFS),
+            mint_quotes: load_or_default(KEY_MINT_QUOTES),
+            melt_quotes: load_or_default(KEY_MELT_QUOTES),
+            transactions: load_or_default(KEY_TRANSACTIONS),
+            sagas: load_or_default(KEY_SAGAS),
+            proof_reservations: load_or_default(KEY_PROOF_RESERVATIONS),
+            melt_quote_reservations: load_or_default(KEY_MELT_QUOTE_RESERVATIONS),
+            mint_quote_reservations: load_or_default(KEY_MINT_QUOTE_RESERVATIONS),
+            kv: load_or_default(KEY_KV),
+        }
+    }
+}
+
+fn kv_key(primary: &str, secondary: &str, key: &str) -> String {
+    format!("{}\0{}\0{}", primary, secondary, key)
+}
+
+// ---------------------------------------------------------------------------
+// Public database type
+// ---------------------------------------------------------------------------
+
+/// LocalStorage-backed wallet database with in-memory write-through cache
+#[derive(Debug)]
+pub struct LocalStorageDatabase {
+    inner: RwLock<Inner>,
+}
+
+impl LocalStorageDatabase {
+    /// Create a new database, loading persisted state from JS storage.
+    pub fn new() -> Self {
+        Self {
+            inner: RwLock::new(Inner::load()),
+        }
+    }
+
+    pub fn into_arc(self) -> Db {
+        Arc::new(self)
+    }
+}
+
+// ---------------------------------------------------------------------------
+// WalletDatabase trait implementation
+// ---------------------------------------------------------------------------
+#[async_trait::async_trait(?Send)]
+impl WalletDatabase<Error> for LocalStorageDatabase {
+    // ---- reads (from memory only) -----------------------------------------
+
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.mints.get(&mint_url).cloned().flatten())
+    }
+
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.mints.clone())
+    }
+
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.mint_keysets.get(&mint_url).cloned())
+    }
+
+    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Error> {
+        let db = self.inner.read().await;
+        for keysets in db.mint_keysets.values() {
+            if let Some(ks) = keysets.iter().find(|k| &k.id == keyset_id) {
+                return Ok(Some(ks.clone()));
+            }
+        }
+        Ok(None)
+    }
+
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.mint_quotes.iter().find(|q| q.id == quote_id).cloned())
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.mint_quotes.clone())
+    }
+
+    async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let db = self.inner.read().await;
+        Ok(db
+            .mint_quotes
+            .iter()
+            .filter(|q| q.amount_issued == cdk_common::Amount::ZERO)
+            .cloned()
+            .collect())
+    }
+
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.melt_quotes.iter().find(|q| q.id == quote_id).cloned())
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.melt_quotes.clone())
+    }
+
+    async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.keys.get(id).cloned())
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        _spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Error> {
+        let db = self.inner.read().await;
+        Ok(db
+            .proofs
+            .iter()
+            .filter(|p| mint_url.as_ref().is_none_or(|u| &p.mint_url == u))
+            .filter(|p| unit.as_ref().is_none_or(|u| &p.unit == u))
+            .filter(|p| state.as_ref().is_none_or(|s| s.contains(&p.state)))
+            .cloned()
+            .collect())
+    }
+
+    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, Error> {
+        let db = self.inner.read().await;
+        Ok(db
+            .proofs
+            .iter()
+            .filter(|p| ys.contains(&p.y))
+            .cloned()
+            .collect())
+    }
+
+    async fn get_balance(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+    ) -> Result<u64, Error> {
+        let proofs = self.get_proofs(mint_url, unit, state, None).await?;
+        Ok(proofs.iter().map(|p| u64::from(p.proof.amount)).sum())
+    }
+
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, Error> {
+        let db = self.inner.read().await;
+        Ok(db
+            .transactions
+            .iter()
+            .find(|t| t.id() == transaction_id)
+            .cloned())
+    }
+
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, Error> {
+        let db = self.inner.read().await;
+        Ok(db
+            .transactions
+            .iter()
+            .filter(|t| mint_url.as_ref().is_none_or(|u| &t.mint_url == u))
+            .filter(|t| direction.as_ref().is_none_or(|d| &t.direction == d))
+            .filter(|t| unit.as_ref().is_none_or(|u| &t.unit == u))
+            .cloned()
+            .collect())
+    }
+
+    // ---- writes (memory + persist) ----------------------------------------
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.proofs.retain(|p| !removed_ys.contains(&p.y));
+        db.proofs.extend(added);
+        persist(KEY_PROOFS, &db.proofs);
+        Ok(())
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: State,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        for proof in db.proofs.iter_mut() {
+            if ys.contains(&proof.y) {
+                proof.state = state;
+            }
+        }
+        persist(KEY_PROOFS, &db.proofs);
+        Ok(())
+    }
+
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.transactions.push(transaction);
+        persist(KEY_TRANSACTIONS, &db.transactions);
+        Ok(())
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        if let Some(info) = db.mints.remove(&old_mint_url) {
+            db.mints.insert(new_mint_url.clone(), info);
+        }
+        if let Some(keysets) = db.mint_keysets.remove(&old_mint_url) {
+            db.mint_keysets.insert(new_mint_url, keysets);
+        }
+        persist(KEY_MINTS, &db.mints);
+        persist(KEY_MINT_KEYSETS, &db.mint_keysets);
+        Ok(())
+    }
+
+    async fn increment_keyset_counter(
+        &self,
+        keyset_id: &Id,
+        count: u32,
+    ) -> Result<u32, Error> {
+        let mut db = self.inner.write().await;
+        let counter = db.keyset_counter.entry(*keyset_id).or_insert(0);
+        let old = *counter;
+        *counter += count;
+        persist(KEY_KEYSET_COUNTER, &db.keyset_counter);
+        Ok(old)
+    }
+
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mints.insert(mint_url, mint_info);
+        persist(KEY_MINTS, &db.mints);
+        Ok(())
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mints.remove(&mint_url);
+        db.mint_keysets.remove(&mint_url);
+        persist(KEY_MINTS, &db.mints);
+        persist(KEY_MINT_KEYSETS, &db.mint_keysets);
+        Ok(())
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mint_keysets
+            .entry(mint_url)
+            .or_default()
+            .extend(keysets);
+        persist(KEY_MINT_KEYSETS, &db.mint_keysets);
+        Ok(())
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mint_quotes.retain(|q| q.id != quote.id);
+        db.mint_quotes.push(quote);
+        persist(KEY_MINT_QUOTES, &db.mint_quotes);
+        Ok(())
+    }
+
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mint_quotes.retain(|q| q.id != quote_id);
+        persist(KEY_MINT_QUOTES, &db.mint_quotes);
+        Ok(())
+    }
+
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.melt_quotes.retain(|q| q.id != quote.id);
+        db.melt_quotes.push(quote);
+        persist(KEY_MELT_QUOTES, &db.melt_quotes);
+        Ok(())
+    }
+
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.melt_quotes.retain(|q| q.id != quote_id);
+        persist(KEY_MELT_QUOTES, &db.melt_quotes);
+        Ok(())
+    }
+
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.keys.insert(keyset.id, keyset.keys);
+        persist(KEY_KEYS, &db.keys);
+        Ok(())
+    }
+
+    async fn remove_keys(&self, id: &Id) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.keys.remove(id);
+        persist(KEY_KEYS, &db.keys);
+        Ok(())
+    }
+
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.transactions.retain(|t| t.id() != transaction_id);
+        persist(KEY_TRANSACTIONS, &db.transactions);
+        Ok(())
+    }
+
+    async fn add_saga(&self, saga: WalletSaga) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.sagas.insert(saga.id.to_string(), saga);
+        persist(KEY_SAGAS, &db.sagas);
+        Ok(())
+    }
+
+    async fn get_saga(&self, id: &uuid::Uuid) -> Result<Option<WalletSaga>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.sagas.get(&id.to_string()).cloned())
+    }
+
+    async fn update_saga(&self, saga: WalletSaga) -> Result<bool, Error> {
+        let mut db = self.inner.write().await;
+        let key = saga.id.to_string();
+        if let Some(existing) = db.sagas.get(&key) {
+            if existing.version == saga.version.saturating_sub(1) {
+                db.sagas.insert(key, saga);
+                persist(KEY_SAGAS, &db.sagas);
+                return Ok(true);
+            }
+            return Ok(false);
+        }
+        Ok(false)
+    }
+
+    async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.sagas.remove(&id.to_string());
+        persist(KEY_SAGAS, &db.sagas);
+        Ok(())
+    }
+
+    async fn get_incomplete_sagas(&self) -> Result<Vec<WalletSaga>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.sagas.values().cloned().collect())
+    }
+
+    async fn reserve_proofs(
+        &self,
+        ys: Vec<PublicKey>,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.proof_reservations.insert(*operation_id, ys);
+        persist(KEY_PROOF_RESERVATIONS, &db.proof_reservations);
+        Ok(())
+    }
+
+    async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.proof_reservations.remove(operation_id);
+        persist(KEY_PROOF_RESERVATIONS, &db.proof_reservations);
+        Ok(())
+    }
+
+    async fn get_reserved_proofs(
+        &self,
+        operation_id: &uuid::Uuid,
+    ) -> Result<Vec<ProofInfo>, Error> {
+        let db = self.inner.read().await;
+        if let Some(ys) = db.proof_reservations.get(operation_id) {
+            Ok(db
+                .proofs
+                .iter()
+                .filter(|p| ys.contains(&p.y))
+                .cloned()
+                .collect())
+        } else {
+            Ok(vec![])
+        }
+    }
+
+    async fn reserve_melt_quote(
+        &self,
+        quote_id: &str,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.melt_quote_reservations
+            .insert(*operation_id, quote_id.to_string());
+        persist(KEY_MELT_QUOTE_RESERVATIONS, &db.melt_quote_reservations);
+        Ok(())
+    }
+
+    async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.melt_quote_reservations.remove(operation_id);
+        persist(KEY_MELT_QUOTE_RESERVATIONS, &db.melt_quote_reservations);
+        Ok(())
+    }
+
+    async fn reserve_mint_quote(
+        &self,
+        quote_id: &str,
+        operation_id: &uuid::Uuid,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mint_quote_reservations
+            .insert(*operation_id, quote_id.to_string());
+        persist(KEY_MINT_QUOTE_RESERVATIONS, &db.mint_quote_reservations);
+        Ok(())
+    }
+
+    async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.mint_quote_reservations.remove(operation_id);
+        persist(KEY_MINT_QUOTE_RESERVATIONS, &db.mint_quote_reservations);
+        Ok(())
+    }
+
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Error> {
+        let db = self.inner.read().await;
+        Ok(db.kv.get(&kv_key(primary_namespace, secondary_namespace, key)).cloned())
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Error> {
+        let db = self.inner.read().await;
+        let prefix = format!("{}\0{}\0", primary_namespace, secondary_namespace);
+        Ok(db
+            .kv
+            .keys()
+            .filter_map(|k| k.strip_prefix(&prefix).map(|s| s.to_string()))
+            .collect())
+    }
+
+    async fn kv_write(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.kv.insert(
+            kv_key(primary_namespace, secondary_namespace, key),
+            value.to_vec(),
+        );
+        persist(KEY_KV, &db.kv);
+        Ok(())
+    }
+
+    async fn kv_remove(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), Error> {
+        let mut db = self.inner.write().await;
+        db.kv.remove(&kv_key(primary_namespace, secondary_namespace, key));
+        persist(KEY_KV, &db.kv);
+        Ok(())
+    }
+}

+ 32 - 0
crates/cdk-wasm/src/logging.rs

@@ -0,0 +1,32 @@
+//! WASM Logging configuration
+//!
+//! Provides functions to initialize tracing for browser console logging.
+
+use std::sync::Once;
+
+use wasm_bindgen::prelude::*;
+
+static INIT: Once = std::sync::Once::new();
+
+/// Initialize tracing for browser console logging.
+///
+/// Call this function once at application startup, before creating
+/// any wallets. Subsequent calls are safe but have no effect.
+///
+/// # Arguments
+///
+/// * `level` - Log level filter (e.g., "debug", "info", "warn", "error", "trace")
+#[wasm_bindgen(js_name = "initLogging")]
+pub fn init_logging(level: String) {
+    INIT.call_once(|| {
+        let _ = level;
+        // Use web_sys::console for basic logging in WASM
+        web_sys::console::log_1(&"cdk-wasm logging initialized".into());
+    });
+}
+
+/// Initialize logging with default "info" level
+#[wasm_bindgen(js_name = "initDefaultLogging")]
+pub fn init_default_logging() {
+    init_logging("info".to_string());
+}

+ 132 - 0
crates/cdk-wasm/src/token.rs

@@ -0,0 +1,132 @@
+//! WASM token bindings
+
+use std::collections::BTreeSet;
+use std::str::FromStr;
+
+use wasm_bindgen::prelude::*;
+
+use crate::error::WasmError;
+use crate::types::{Amount, MintUrl, Proofs};
+
+/// WASM-compatible Token
+#[wasm_bindgen]
+pub struct Token {
+    pub(crate) inner: cdk::nuts::Token,
+}
+
+impl std::fmt::Debug for Token {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Token({})", self.inner)
+    }
+}
+
+impl std::fmt::Display for Token {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.inner)
+    }
+}
+
+impl FromStr for Token {
+    type Err = WasmError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let token = cdk::nuts::Token::from_str(s)
+            .map_err(|e| WasmError::internal(format!("Invalid token: {}", e)))?;
+        Ok(Token { inner: token })
+    }
+}
+
+impl From<cdk::nuts::Token> for Token {
+    fn from(token: cdk::nuts::Token) -> Self {
+        Self { inner: token }
+    }
+}
+
+impl From<Token> for cdk::nuts::Token {
+    fn from(token: Token) -> Self {
+        token.inner
+    }
+}
+
+#[wasm_bindgen]
+impl Token {
+    /// Create a new Token from string
+    #[wasm_bindgen(js_name = "fromString")]
+    pub fn from_string(encoded_token: String) -> Result<Token, WasmError> {
+        let token = cdk::nuts::Token::from_str(&encoded_token)
+            .map_err(|e| WasmError::internal(format!("Invalid token: {}", e)))?;
+        Ok(Token { inner: token })
+    }
+
+    /// Get the total value of the token
+    pub fn value(&self) -> Result<Amount, WasmError> {
+        Ok(self.inner.value()?.into())
+    }
+
+    /// Get the memo from the token
+    pub fn memo(&self) -> Option<String> {
+        self.inner.memo().clone()
+    }
+
+    /// Get the mint URL
+    #[wasm_bindgen(js_name = "mintUrl")]
+    pub fn mint_url(&self) -> Result<JsValue, WasmError> {
+        let url: MintUrl = self.inner.mint_url()?.into();
+        serde_wasm_bindgen::to_value(&url).map_err(WasmError::internal)
+    }
+
+    /// Get proofs from the token (simplified - no keyset filtering)
+    #[wasm_bindgen(js_name = "proofsSimple")]
+    pub fn proofs_simple(&self) -> Result<JsValue, WasmError> {
+        let empty_keysets = vec![];
+        let proofs = self.inner.proofs(&empty_keysets)?;
+        let ffi_proofs: Proofs = proofs.into_iter().map(|p| p.into()).collect();
+        serde_wasm_bindgen::to_value(&ffi_proofs).map_err(WasmError::internal)
+    }
+
+    /// Convert token to raw bytes
+    #[wasm_bindgen(js_name = "toRawBytes")]
+    pub fn to_raw_bytes(&self) -> Result<Vec<u8>, WasmError> {
+        Ok(self.inner.to_raw_bytes()?)
+    }
+
+    /// Encode token to string representation
+    pub fn encode(&self) -> String {
+        self.to_string()
+    }
+
+    /// Decode token from raw bytes
+    #[wasm_bindgen(js_name = "fromRawBytes")]
+    pub fn from_raw_bytes(bytes: Vec<u8>) -> Result<Token, WasmError> {
+        let token = cdk::nuts::Token::try_from(&bytes)?;
+        Ok(Token { inner: token })
+    }
+
+    /// Decode token from string representation
+    pub fn decode(encoded_token: String) -> Result<Token, WasmError> {
+        encoded_token.parse()
+    }
+
+    /// Return all P2PK pubkeys referenced by this token's spending conditions
+    #[wasm_bindgen(js_name = "p2pkPubkeys")]
+    pub fn p2pk_pubkeys(&self) -> Vec<String> {
+        let set = self
+            .inner
+            .p2pk_pubkeys()
+            .map(|keys| {
+                keys.into_iter()
+                    .map(|k| k.to_string())
+                    .collect::<BTreeSet<_>>()
+            })
+            .unwrap_or_default();
+        set.into_iter().collect()
+    }
+
+    /// Return all locktimes from spending conditions (sorted ascending)
+    pub fn locktimes(&self) -> Vec<u64> {
+        self.inner
+            .locktimes()
+            .map(|s| s.into_iter().collect())
+            .unwrap_or_default()
+    }
+}

+ 118 - 0
crates/cdk-wasm/src/types/amount.rs

@@ -0,0 +1,118 @@
+//! Amount and currency related types
+
+use cdk::nuts::CurrencyUnit as CdkCurrencyUnit;
+use cdk::Amount as CdkAmount;
+use serde::{Deserialize, Serialize};
+use wasm_bindgen::prelude::*;
+
+/// WASM-compatible Amount type
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(transparent)]
+#[wasm_bindgen]
+pub struct Amount {
+    #[wasm_bindgen(readonly)]
+    pub value: u64,
+}
+
+#[wasm_bindgen]
+impl Amount {
+    #[wasm_bindgen(constructor)]
+    pub fn new(value: u64) -> Self {
+        Self { value }
+    }
+
+    pub fn zero() -> Self {
+        Self { value: 0 }
+    }
+
+    #[wasm_bindgen(js_name = "isZero")]
+    pub fn is_zero(&self) -> bool {
+        self.value == 0
+    }
+}
+
+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)
+    }
+}
+
+/// WASM-compatible Currency Unit
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+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,
+        }
+    }
+}
+
+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),
+        }
+    }
+}
+
+/// WASM-compatible SplitTarget
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum SplitTarget {
+    None,
+    Value { amount: Amount },
+    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(),
+            },
+        }
+    }
+}

+ 85 - 0
crates/cdk-wasm/src/types/invoice.rs

@@ -0,0 +1,85 @@
+//! Invoice decoding WASM types and functions
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::WasmError;
+
+/// Type of Lightning payment request
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum PaymentType {
+    /// Bolt11 invoice
+    Bolt11,
+    /// Bolt12 offer
+    Bolt12,
+}
+
+impl From<cdk::invoice::PaymentType> for PaymentType {
+    fn from(payment_type: cdk::invoice::PaymentType) -> Self {
+        match payment_type {
+            cdk::invoice::PaymentType::Bolt11 => Self::Bolt11,
+            cdk::invoice::PaymentType::Bolt12 => Self::Bolt12,
+        }
+    }
+}
+
+impl From<PaymentType> for cdk::invoice::PaymentType {
+    fn from(payment_type: PaymentType) -> Self {
+        match payment_type {
+            PaymentType::Bolt11 => Self::Bolt11,
+            PaymentType::Bolt12 => Self::Bolt12,
+        }
+    }
+}
+
+/// Decoded invoice or offer information
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DecodedInvoice {
+    /// Type of payment request (Bolt11 or Bolt12)
+    pub payment_type: PaymentType,
+    /// Amount in millisatoshis, if specified
+    pub amount_msat: Option<u64>,
+    /// Expiry timestamp (Unix timestamp), if specified
+    pub expiry: Option<u64>,
+    /// Description or offer description, if specified
+    pub description: Option<String>,
+}
+
+impl From<cdk::invoice::DecodedInvoice> for DecodedInvoice {
+    fn from(decoded: cdk::invoice::DecodedInvoice) -> Self {
+        Self {
+            payment_type: decoded.payment_type.into(),
+            amount_msat: decoded.amount_msat,
+            expiry: decoded.expiry,
+            description: decoded.description,
+        }
+    }
+}
+
+impl From<DecodedInvoice> for cdk::invoice::DecodedInvoice {
+    fn from(decoded: DecodedInvoice) -> Self {
+        Self {
+            payment_type: decoded.payment_type.into(),
+            amount_msat: decoded.amount_msat,
+            expiry: decoded.expiry,
+            description: decoded.description,
+        }
+    }
+}
+
+/// Decode a bolt11 invoice or bolt12 offer from a string
+///
+/// This function attempts to parse the input as a bolt11 invoice first,
+/// then as a bolt12 offer if bolt11 parsing fails.
+///
+/// # Arguments
+///
+/// * `invoice_str` - The invoice or offer string to decode
+///
+/// # Returns
+///
+/// * `Ok(DecodedInvoice)` - Successfully decoded invoice/offer information
+/// * `Err(WasmError)` - Failed to parse as either bolt11 or bolt12
+pub fn decode_invoice(invoice_str: String) -> Result<DecodedInvoice, WasmError> {
+    let decoded = cdk::invoice::decode_invoice(&invoice_str)?;
+    Ok(decoded.into())
+}

+ 258 - 0
crates/cdk-wasm/src/types/keys.rs

@@ -0,0 +1,258 @@
+//! Key-related WASM types
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::CurrencyUnit;
+use crate::error::WasmError;
+
+/// WASM-compatible KeySetInfo
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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).expect("Invalid keyset ID"),
+            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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode KeySetInfo from JSON string
+pub fn decode_key_set_info(json: String) -> Result<KeySetInfo, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode KeySetInfo to JSON string
+pub fn encode_key_set_info(info: KeySetInfo) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&info)?)
+}
+
+/// WASM-compatible PublicKey
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[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 = WasmError;
+
+    fn try_from(key: PublicKey) -> Result<Self, Self::Error> {
+        key.hex
+            .parse()
+            .map_err(|e| WasmError::internal(format!("Invalid public key: {}", e)))
+    }
+}
+
+/// WASM-compatible Keys (simplified - contains only essential info)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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| WasmError::internal(format!("Invalid public key: {}", e)))?;
+            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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Keys from JSON string
+pub fn decode_keys(json: String) -> Result<Keys, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Keys to JSON string
+pub fn encode_keys(keys: Keys) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&keys)?)
+}
+
+/// WASM-compatible KeySet
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KeySet {
+    /// Keyset ID
+    pub id: String,
+    /// Currency unit
+    pub unit: CurrencyUnit,
+    /// Keyset state - indicates whether the mint will sign new outputs with this keyset
+    pub active: Option<bool>,
+    /// Input fee in parts per thousand (ppk) per input spent from this keyset
+    pub input_fee_ppk: u64,
+    /// 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(),
+            active: keyset.active,
+            input_fee_ppk: keyset.input_fee_ppk,
+            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 = WasmError;
+
+    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| WasmError::internal(format!("Invalid keyset ID: {}", e)))?;
+
+        // 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| WasmError::internal(format!("Invalid public key: {}", e)))?;
+            keys_map.insert(amount, pubkey);
+        }
+        let keys = cdk::nuts::Keys::new(keys_map);
+
+        Ok(cdk::nuts::KeySet {
+            id,
+            unit,
+            active: keyset.active,
+            input_fee_ppk: keyset.input_fee_ppk,
+            keys,
+            final_expiry: keyset.final_expiry,
+        })
+    }
+}
+
+impl KeySet {
+    /// Convert KeySet to JSON string
+    pub fn to_json(&self) -> Result<String, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode KeySet from JSON string
+pub fn decode_key_set(json: String) -> Result<KeySet, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode KeySet to JSON string
+pub fn encode_key_set(keyset: KeySet) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&keyset)?)
+}
+
+/// WASM-compatible Id (for keyset IDs)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[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).expect("Invalid ID hex")
+    }
+}

+ 685 - 0
crates/cdk-wasm/src/types/mint.rs

@@ -0,0 +1,685 @@
+//! Mint-related WASM types
+
+use std::str::FromStr;
+
+use cdk::nuts::nut00::{KnownMethod, PaymentMethod as NutPaymentMethod};
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::quote::PaymentMethod;
+use crate::error::WasmError;
+
+/// WASM-compatible Mint URL
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct MintUrl {
+    pub url: String,
+}
+
+impl MintUrl {
+    pub fn new(url: String) -> Result<Self, WasmError> {
+        // Validate URL format
+        url::Url::parse(&url).map_err(|e| WasmError::internal(format!("Invalid URL: {}", e)))?;
+
+        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 = WasmError;
+
+    fn try_from(mint_url: MintUrl) -> Result<Self, Self::Error> {
+        cdk::mint_url::MintUrl::from_str(&mint_url.url)
+            .map_err(|e| WasmError::internal(format!("Invalid URL: {}", e)))
+    }
+}
+
+/// WASM-compatible MintVersion
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MintVersion from JSON string
+pub fn decode_mint_version(json: String) -> Result<MintVersion, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode MintVersion to JSON string
+pub fn encode_mint_version(version: MintVersion) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&version)?)
+}
+
+/// WASM-compatible ContactInfo
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ContactInfo from JSON string
+pub fn decode_contact_info(json: String) -> Result<ContactInfo, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ContactInfo to JSON string
+pub fn encode_contact_info(info: ContactInfo) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&info)?)
+}
+
+/// WASM-compatible SupportedSettings
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[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 WASM Types
+// -----------------------------
+
+/// WASM-compatible MintMethodSettings (NUT-04)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    fn try_from(s: MintMethodSettings) -> Result<Self, Self::Error> {
+        let options = match s.method {
+            PaymentMethod::Bolt11 => s
+                .description
+                .map(|description| cdk::nuts::nut04::MintMethodOptions::Bolt11 { description }),
+            PaymentMethod::Custom { .. } => Some(cdk::nuts::nut04::MintMethodOptions::Custom {}),
+            _ => 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,
+        })
+    }
+}
+
+/// WASM-compatible Nut04 Settings
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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,
+        })
+    }
+}
+
+/// WASM-compatible MeltMethodSettings (NUT-05)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    fn try_from(s: MeltMethodSettings) -> Result<Self, Self::Error> {
+        let options = match s.method {
+            PaymentMethod::Bolt11 => s
+                .amountless
+                .map(|amountless| 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,
+        })
+    }
+}
+
+/// WASM-compatible Nut05 Settings
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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,
+        })
+    }
+}
+
+/// WASM-compatible ProtectedEndpoint (for auth nuts)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ProtectedEndpoint {
+    /// HTTP method (GET, POST, etc.)
+    pub method: String,
+    /// Endpoint path
+    pub path: String,
+}
+
+/// WASM-compatible ClearAuthSettings (NUT-21)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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>,
+}
+
+/// WASM-compatible BlindAuthSettings (NUT-22)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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 = WasmError;
+
+    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 = WasmError;
+
+    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(WasmError::internal(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::MintQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+            ),
+            "/v1/mint/bolt11" => {
+                cdk::nuts::RoutePath::Mint(NutPaymentMethod::Known(KnownMethod::Bolt11).to_string())
+            }
+            "/v1/melt/quote/bolt11" => cdk::nuts::RoutePath::MeltQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt11).to_string(),
+            ),
+            "/v1/melt/bolt11" => {
+                cdk::nuts::RoutePath::Melt(NutPaymentMethod::Known(KnownMethod::Bolt11).to_string())
+            }
+            "/v1/swap" => cdk::nuts::RoutePath::Swap,
+            "/v1/ws" => cdk::nuts::RoutePath::Ws,
+            "/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::MintQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt12).to_string(),
+            ),
+            "/v1/mint/bolt12" => {
+                cdk::nuts::RoutePath::Mint(NutPaymentMethod::Known(KnownMethod::Bolt12).to_string())
+            }
+            "/v1/melt/quote/bolt12" => cdk::nuts::RoutePath::MeltQuote(
+                NutPaymentMethod::Known(KnownMethod::Bolt12).to_string(),
+            ),
+            "/v1/melt/bolt12" => {
+                cdk::nuts::RoutePath::Melt(NutPaymentMethod::Known(KnownMethod::Bolt12).to_string())
+            }
+            _ => {
+                return Err(WasmError::internal(format!(
+                    "Unknown route path: {}",
+                    endpoint.path
+                )))
+            }
+        };
+
+        Ok(cdk::nuts::ProtectedEndpoint::new(method, route_path))
+    }
+}
+
+/// WASM-compatible Nuts settings (extended to include NUT-04 and NUT-05 settings)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Nuts from JSON string
+pub fn decode_nuts(json: String) -> Result<Nuts, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Nuts to JSON string
+pub fn encode_nuts(nuts: Nuts) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&nuts)?)
+}
+
+/// WASM-compatible MintInfo
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 WASM 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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MintInfo from JSON string
+pub fn decode_mint_info(json: String) -> Result<MintInfo, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode MintInfo to JSON string
+pub fn encode_mint_info(info: MintInfo) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&info)?)
+}

+ 26 - 0
crates/cdk-wasm/src/types/mod.rs

@@ -0,0 +1,26 @@
+//! WASM-compatible types
+//!
+//! This module contains all the types used by the WASM bindings.
+//! Types are organized into logical submodules for better maintainability.
+
+pub mod amount;
+pub mod invoice;
+pub mod keys;
+pub mod mint;
+pub mod payment_request;
+pub mod proof;
+pub mod quote;
+pub mod subscription;
+pub mod transaction;
+pub mod wallet;
+
+pub use amount::*;
+pub use invoice::*;
+pub use keys::*;
+pub use mint::*;
+pub use payment_request::*;
+pub use proof::*;
+pub use quote::*;
+pub use subscription::*;
+pub use transaction::*;
+pub use wallet::*;

+ 164 - 0
crates/cdk-wasm/src/types/payment_request.rs

@@ -0,0 +1,164 @@
+//! Payment Request WASM types (NUT-18)
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::mint::MintUrl;
+use super::proof::Proof;
+
+/// Transport type for payment request delivery
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TransportType {
+    Nostr,
+    HttpPost,
+}
+
+impl From<cdk::nuts::TransportType> for TransportType {
+    fn from(t: cdk::nuts::TransportType) -> Self {
+        match t {
+            cdk::nuts::TransportType::Nostr => TransportType::Nostr,
+            cdk::nuts::TransportType::HttpPost => TransportType::HttpPost,
+        }
+    }
+}
+
+impl From<TransportType> for cdk::nuts::TransportType {
+    fn from(t: TransportType) -> Self {
+        match t {
+            TransportType::Nostr => cdk::nuts::TransportType::Nostr,
+            TransportType::HttpPost => cdk::nuts::TransportType::HttpPost,
+        }
+    }
+}
+
+/// Transport for payment request delivery
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Transport {
+    pub transport_type: TransportType,
+    pub target: String,
+    pub tags: Option<Vec<Vec<String>>>,
+}
+
+impl From<cdk::nuts::Transport> for Transport {
+    fn from(t: cdk::nuts::Transport) -> Self {
+        Self {
+            transport_type: t._type.into(),
+            target: t.target,
+            tags: t.tags,
+        }
+    }
+}
+
+impl From<Transport> for cdk::nuts::Transport {
+    fn from(t: Transport) -> Self {
+        Self {
+            _type: t.transport_type.into(),
+            target: t.target,
+            tags: t.tags,
+        }
+    }
+}
+
+/// NUT-18 Payment Request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PaymentRequest {
+    pub payment_id: Option<String>,
+    pub amount: Option<Amount>,
+    pub unit: Option<CurrencyUnit>,
+    pub single_use: Option<bool>,
+    pub mints: Option<Vec<String>>,
+    pub description: Option<String>,
+    pub transports: Vec<Transport>,
+}
+
+impl From<cdk::nuts::PaymentRequest> for PaymentRequest {
+    fn from(req: cdk::nuts::PaymentRequest) -> Self {
+        Self {
+            payment_id: req.payment_id,
+            amount: req.amount.map(Into::into),
+            unit: req.unit.map(Into::into),
+            single_use: req.single_use,
+            mints: req
+                .mints
+                .map(|mints| mints.iter().map(|m| m.to_string()).collect()),
+            description: req.description,
+            transports: req.transports.into_iter().map(Into::into).collect(),
+        }
+    }
+}
+
+/// Parameters for creating a NUT-18 payment request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateRequestParams {
+    pub amount: Option<u64>,
+    pub unit: String,
+    pub description: Option<String>,
+    pub pubkeys: Option<Vec<String>>,
+    pub num_sigs: u64,
+    pub hash: Option<String>,
+    pub preimage: Option<String>,
+    pub transport: String,
+    pub http_url: Option<String>,
+    pub nostr_relays: Option<Vec<String>>,
+}
+
+impl Default for CreateRequestParams {
+    fn default() -> Self {
+        Self {
+            amount: None,
+            unit: "sat".to_string(),
+            description: None,
+            pubkeys: None,
+            num_sigs: 1,
+            hash: None,
+            preimage: None,
+            transport: "none".to_string(),
+            http_url: None,
+            nostr_relays: None,
+        }
+    }
+}
+
+impl From<CreateRequestParams> for cdk::wallet::payment_request::CreateRequestParams {
+    fn from(params: CreateRequestParams) -> Self {
+        Self {
+            amount: params.amount,
+            unit: params.unit,
+            description: params.description,
+            pubkeys: params.pubkeys,
+            num_sigs: params.num_sigs,
+            hash: params.hash,
+            preimage: params.preimage,
+            transport: params.transport,
+            http_url: params.http_url,
+            nostr_relays: params.nostr_relays,
+        }
+    }
+}
+
+impl From<cdk::wallet::payment_request::CreateRequestParams> for CreateRequestParams {
+    fn from(params: cdk::wallet::payment_request::CreateRequestParams) -> Self {
+        Self {
+            amount: params.amount,
+            unit: params.unit,
+            description: params.description,
+            pubkeys: params.pubkeys,
+            num_sigs: params.num_sigs,
+            hash: params.hash,
+            preimage: params.preimage,
+            transport: params.transport,
+            http_url: params.http_url,
+            nostr_relays: params.nostr_relays,
+        }
+    }
+}
+
+/// Payment request payload sent over transport
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PaymentRequestPayload {
+    pub id: Option<String>,
+    pub memo: Option<String>,
+    pub mint: MintUrl,
+    pub unit: CurrencyUnit,
+    pub proofs: Vec<Proof>,
+}

+ 552 - 0
crates/cdk-wasm/src/types/proof.rs

@@ -0,0 +1,552 @@
+//! Proof-related WASM 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::WasmError;
+
+/// WASM-compatible Proof state
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+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,
+        }
+    }
+}
+
+/// WASM-compatible Proof
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Proof {
+    /// Proof amount
+    pub amount: Amount,
+    /// Secret (as string)
+    pub secret: String,
+    /// Unblinded signature C (as hex string)
+    pub c: String,
+    /// Keyset ID (as hex string)
+    pub keyset_id: String,
+    /// Optional witness
+    pub witness: Option<Witness>,
+    /// Optional DLEQ proof
+    pub dleq: Option<ProofDleq>,
+}
+
+impl From<cdk::nuts::Proof> for Proof {
+    fn from(proof: cdk::nuts::Proof) -> Self {
+        Self {
+            amount: proof.amount.into(),
+            secret: proof.secret.to_string(),
+            c: proof.c.to_string(),
+            keyset_id: proof.keyset_id.to_string(),
+            witness: proof.witness.map(|w| w.into()),
+            dleq: proof.dleq.map(|d| d.into()),
+        }
+    }
+}
+
+impl TryFrom<Proof> for cdk::nuts::Proof {
+    type Error = WasmError;
+
+    fn try_from(proof: Proof) -> Result<Self, Self::Error> {
+        use std::str::FromStr;
+
+        use cdk::nuts::Id;
+
+        Ok(Self {
+            amount: proof.amount.into(),
+            secret: cdk::secret::Secret::from_str(&proof.secret)
+                .map_err(|e| WasmError::internal(format!("Invalid secret: {}", e)))?,
+            c: cdk::nuts::PublicKey::from_str(&proof.c)
+                .map_err(|e| WasmError::internal(format!("Invalid public key: {}", e)))?,
+            keyset_id: Id::from_str(&proof.keyset_id)
+                .map_err(|e| WasmError::internal(format!("Invalid keyset ID: {}", e)))?,
+            witness: proof.witness.map(|w| w.into()),
+            dleq: proof.dleq.map(|d| d.into()),
+        })
+    }
+}
+
+/// Get the Y value (hash_to_curve of secret) for a proof
+pub fn proof_y(proof: &Proof) -> Result<String, WasmError> {
+    // Convert to CDK proof to calculate Y
+    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
+    Ok(cdk_proof.y()?.to_string())
+}
+
+/// Check if proof is active with given keyset IDs
+pub fn proof_is_active(proof: &Proof, 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();
+
+    // A proof is active if its keyset_id is in the active list
+    if let Ok(keyset_id) = Id::from_str(&proof.keyset_id) {
+        ids.contains(&keyset_id)
+    } else {
+        false
+    }
+}
+
+/// Check if proof has DLEQ proof
+pub fn proof_has_dleq(proof: &Proof) -> bool {
+    proof.dleq.is_some()
+}
+
+/// Verify HTLC witness on a proof
+pub fn proof_verify_htlc(proof: &Proof) -> Result<(), WasmError> {
+    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
+    cdk_proof.verify_htlc().map_err(WasmError::internal)
+}
+
+/// Verify DLEQ proof on a proof
+pub fn proof_verify_dleq(
+    proof: &Proof,
+    mint_pubkey: super::keys::PublicKey,
+) -> Result<(), WasmError> {
+    let cdk_proof: cdk::nuts::Proof = proof.clone().try_into()?;
+    let cdk_pubkey: cdk::nuts::PublicKey = mint_pubkey.try_into()?;
+    cdk_proof
+        .verify_dleq(cdk_pubkey)
+        .map_err(WasmError::internal)
+}
+
+/// Sign a P2PK proof with a secret key, returning a new signed proof
+pub fn proof_sign_p2pk(proof: Proof, secret_key_hex: String) -> Result<Proof, WasmError> {
+    let mut cdk_proof: cdk::nuts::Proof = proof.try_into()?;
+    let secret_key = cdk::nuts::SecretKey::from_hex(&secret_key_hex)
+        .map_err(|e| WasmError::internal(format!("Invalid secret key: {}", e)))?;
+
+    cdk_proof
+        .sign_p2pk(secret_key)
+        .map_err(WasmError::internal)?;
+
+    Ok(cdk_proof.into())
+}
+
+/// WASM-compatible Proofs (vector of Proof)
+pub type Proofs = Vec<Proof>;
+
+/// WASM-compatible DLEQ proof for proofs
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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,
+}
+
+/// WASM-compatible DLEQ proof for blind signatures
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 function to calculate total amount of proofs
+pub fn proofs_total_amount(proofs: &Proofs) -> Result<Amount, WasmError> {
+    let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+        proofs.iter().map(|p| p.clone().try_into()).collect();
+    let cdk_proofs = cdk_proofs?;
+    use cdk::nuts::ProofsMethods;
+    Ok(cdk_proofs.total_amount()?.into())
+}
+
+/// WASM-compatible Conditions (for spending conditions)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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| WasmError::internal(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| WasmError::internal(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(WasmError::internal("Invalid sig_flag value")),
+        };
+
+        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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Conditions from JSON string
+pub fn decode_conditions(json: String) -> Result<Conditions, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Conditions to JSON string
+pub fn encode_conditions(conditions: Conditions) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&conditions)?)
+}
+
+/// WASM-compatible Witness
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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,
+            }),
+        }
+    }
+}
+
+/// WASM-compatible SpendingConditions
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    fn try_from(spending_conditions: SpendingConditions) -> Result<Self, Self::Error> {
+        match spending_conditions {
+            SpendingConditions::P2PK { pubkey, conditions } => {
+                let pubkey = pubkey
+                    .parse()
+                    .map_err(|e| WasmError::internal(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| WasmError::internal(format!("Invalid hash: {}", e)))?;
+                let conditions = conditions.map(|c| c.try_into()).transpose()?;
+                Ok(Self::HTLCConditions {
+                    data: hash,
+                    conditions,
+                })
+            }
+        }
+    }
+}
+
+/// WASM-compatible ProofInfo
+#[derive(Debug, Clone)]
+pub struct ProofInfo {
+    /// Proof
+    pub proof: 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,
+    /// Operation ID that is using/spending this proof
+    pub used_by_operation: Option<String>,
+    /// Operation ID that created this proof
+    pub created_by_operation: Option<String>,
+}
+
+impl From<cdk::types::ProofInfo> for ProofInfo {
+    fn from(info: cdk::types::ProofInfo) -> Self {
+        Self {
+            proof: 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(),
+            used_by_operation: info.used_by_operation.map(|u| u.to_string()),
+            created_by_operation: info.created_by_operation.map(|u| u.to_string()),
+        }
+    }
+}
+
+/// Decode ProofInfo from JSON string
+pub fn decode_proof_info(json: String) -> Result<ProofInfo, WasmError> {
+    let info: cdk::types::ProofInfo = serde_json::from_str(&json)?;
+    Ok(info.into())
+}
+
+/// Encode ProofInfo to JSON string
+pub fn encode_proof_info(info: ProofInfo) -> Result<String, WasmError> {
+    use std::str::FromStr;
+    // Convert to cdk::types::ProofInfo for serialization
+    let cdk_info = cdk::types::ProofInfo {
+        proof: info.proof.try_into()?,
+        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(),
+        used_by_operation: info
+            .used_by_operation
+            .map(|id| uuid::Uuid::from_str(&id))
+            .transpose()
+            .map_err(|e| WasmError::internal(e.to_string()))?,
+        created_by_operation: info
+            .created_by_operation
+            .map(|id| uuid::Uuid::from_str(&id))
+            .transpose()
+            .map_err(|e| WasmError::internal(e.to_string()))?,
+    };
+    Ok(serde_json::to_string(&cdk_info)?)
+}
+
+/// WASM-compatible ProofStateUpdate
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ProofStateUpdate from JSON string
+pub fn decode_proof_state_update(json: String) -> Result<ProofStateUpdate, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ProofStateUpdate to JSON string
+pub fn encode_proof_state_update(update: ProofStateUpdate) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&update)?)
+}

+ 470 - 0
crates/cdk-wasm/src/types/quote.rs

@@ -0,0 +1,470 @@
+//! Quote-related WASM types
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, CurrencyUnit};
+use super::mint::MintUrl;
+use crate::error::WasmError;
+
+/// WASM-compatible MintQuote
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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>,
+    /// Operation ID that reserved this quote
+    pub used_by_operation: Option<String>,
+    /// Version for optimistic locking
+    #[serde(default)]
+    pub version: u32,
+}
+
+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()),
+            used_by_operation: quote.used_by_operation.map(|id| id.to_string()),
+            version: quote.version,
+        }
+    }
+}
+
+impl TryFrom<MintQuote> for cdk::wallet::MintQuote {
+    type Error = WasmError;
+
+    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| WasmError::internal(format!("Invalid secret key: {}", e)))?;
+
+        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,
+            used_by_operation: quote.used_by_operation,
+            version: quote.version,
+        })
+    }
+}
+
+/// Get total amount for a mint quote (amount paid)
+pub fn mint_quote_total_amount(quote: &MintQuote) -> Result<Amount, WasmError> {
+    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
+    Ok(cdk_quote.total_amount().into())
+}
+
+/// Check if mint quote is expired
+pub fn mint_quote_is_expired(quote: &MintQuote, current_time: u64) -> Result<bool, WasmError> {
+    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
+    Ok(cdk_quote.is_expired(current_time))
+}
+
+/// Get amount that can be minted from a mint quote
+pub fn mint_quote_amount_mintable(quote: &MintQuote) -> Result<Amount, WasmError> {
+    let cdk_quote: cdk::wallet::MintQuote = quote.clone().try_into()?;
+    Ok(cdk_quote.amount_mintable().into())
+}
+
+/// Decode MintQuote from JSON string
+pub fn decode_mint_quote(json: String) -> Result<MintQuote, WasmError> {
+    let quote: cdk::wallet::MintQuote = serde_json::from_str(&json)?;
+    Ok(quote.into())
+}
+
+/// Encode MintQuote to JSON string
+pub fn encode_mint_quote(quote: MintQuote) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&quote)?)
+}
+
+/// WASM-compatible MintQuoteBolt11Response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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()),
+        }
+    }
+}
+
+impl From<cdk::wallet::MintQuote> for MintQuoteBolt11Response {
+    fn from(quote: cdk::wallet::MintQuote) -> Self {
+        Self {
+            quote: quote.id,
+            request: quote.request,
+            state: quote.state.into(),
+            expiry: Some(quote.expiry),
+            amount: quote.amount.map(Into::into),
+            unit: Some(quote.unit.into()),
+            pubkey: quote.secret_key.map(|sk| sk.public_key().to_string()),
+        }
+    }
+}
+
+/// WASM-compatible MintQuoteCustomResponse
+///
+/// This is a unified response type for custom payment methods that includes
+/// extra fields for method-specific data (e.g., ehash share).
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MintQuoteCustomResponse {
+    /// 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>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    pub extra: Option<String>,
+}
+
+impl From<cdk::nuts::MintQuoteCustomResponse<String>> for MintQuoteCustomResponse {
+    fn from(response: cdk::nuts::MintQuoteCustomResponse<String>) -> Self {
+        let extra = if response.extra.is_null() {
+            None
+        } else {
+            Some(response.extra.to_string())
+        };
+
+        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()),
+            extra,
+        }
+    }
+}
+
+/// WASM-compatible MeltQuoteBolt11Response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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),
+        }
+    }
+}
+
+/// WASM-compatible MeltQuoteCustomResponse
+///
+/// This is a unified response type for custom payment methods that includes
+/// extra fields for method-specific data.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MeltQuoteCustomResponse {
+    /// 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>,
+    /// Extra payment-method-specific fields as JSON string
+    ///
+    /// These fields are flattened into the JSON representation, allowing
+    /// custom payment methods to include additional data without nesting.
+    pub extra: Option<String>,
+}
+
+impl From<cdk::nuts::MeltQuoteCustomResponse<String>> for MeltQuoteCustomResponse {
+    fn from(response: cdk::nuts::MeltQuoteCustomResponse<String>) -> Self {
+        let extra = if response.extra.is_null() {
+            None
+        } else {
+            Some(response.extra.to_string())
+        };
+
+        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),
+            extra,
+        }
+    }
+}
+
+/// WASM-compatible PaymentMethod
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+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.as_str() {
+            "bolt11" => Self::Bolt11,
+            "bolt12" => Self::Bolt12,
+            s => Self::Custom {
+                method: s.to_string(),
+            },
+        }
+    }
+}
+
+impl From<PaymentMethod> for cdk::nuts::PaymentMethod {
+    fn from(method: PaymentMethod) -> Self {
+        match method {
+            PaymentMethod::Bolt11 => Self::from("bolt11"),
+            PaymentMethod::Bolt12 => Self::from("bolt12"),
+            PaymentMethod::Custom { method } => Self::from(method),
+        }
+    }
+}
+
+/// WASM-compatible MeltQuote
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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,
+    /// Operation ID that reserved this quote
+    pub used_by_operation: Option<String>,
+    /// Version for optimistic locking
+    #[serde(default)]
+    pub version: u32,
+}
+
+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(),
+            used_by_operation: quote.used_by_operation.map(|id| id.to_string()),
+            version: quote.version,
+        }
+    }
+}
+
+impl TryFrom<MeltQuote> for cdk::wallet::MeltQuote {
+    type Error = WasmError;
+
+    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(),
+            used_by_operation: quote.used_by_operation,
+            version: quote.version,
+        })
+    }
+}
+
+impl MeltQuote {
+    /// Convert MeltQuote to JSON string
+    pub fn to_json(&self) -> Result<String, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode MeltQuote from JSON string
+pub fn decode_melt_quote(json: String) -> Result<MeltQuote, WasmError> {
+    let quote: cdk::wallet::MeltQuote = serde_json::from_str(&json)?;
+    Ok(quote.into())
+}
+
+/// Encode MeltQuote to JSON string
+pub fn encode_melt_quote(quote: MeltQuote) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&quote)?)
+}
+
+/// WASM-compatible QuoteState
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+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

+ 102 - 0
crates/cdk-wasm/src/types/subscription.rs

@@ -0,0 +1,102 @@
+//! Subscription-related WASM types
+
+use std::sync::Arc;
+
+use serde::{Deserialize, Serialize};
+
+use super::proof::ProofStateUpdate;
+use super::quote::{MeltQuoteBolt11Response, MintQuoteBolt11Response};
+use crate::error::WasmError;
+
+/// WASM-compatible SubscriptionKind
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SubscriptionKind {
+    Bolt11MeltQuote,
+    Bolt11MintQuote,
+    Bolt12MintQuote,
+    Bolt12MeltQuote,
+    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::Bolt12MeltQuote => cdk::nuts::nut17::Kind::Bolt12MeltQuote,
+            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::Bolt12MeltQuote => SubscriptionKind::Bolt12MeltQuote,
+            cdk::nuts::nut17::Kind::ProofState => SubscriptionKind::ProofState,
+            _ => SubscriptionKind::ProofState,
+        }
+    }
+}
+
+/// WASM-compatible SubscribeParams
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SubscribeParams {
+    pub kind: SubscriptionKind,
+    pub filters: Vec<String>,
+    pub id: Option<String>,
+}
+
+impl From<SubscribeParams> for cdk::nuts::nut17::Params<Arc<String>> {
+    fn from(params: SubscribeParams) -> Self {
+        let sub_id = params.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
+
+        cdk::nuts::nut17::Params {
+            kind: params.kind.into(),
+            filters: params.filters,
+            id: Arc::new(sub_id),
+        }
+    }
+}
+
+impl SubscribeParams {
+    /// Convert SubscribeParams to JSON string
+    pub fn to_json(&self) -> Result<String, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// WASM-compatible NotificationPayload
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum NotificationPayload {
+    ProofState { proof_states: Vec<ProofStateUpdate> },
+    MintQuoteUpdate { quote: MintQuoteBolt11Response },
+    MeltQuoteUpdate { quote: MeltQuoteBolt11Response },
+}
+
+impl From<cdk::event::MintEvent<String>> for NotificationPayload {
+    fn from(payload: cdk::event::MintEvent<String>) -> Self {
+        match payload.into() {
+            cdk::nuts::NotificationPayload::ProofState(states) => NotificationPayload::ProofState {
+                proof_states: vec![states.into()],
+            },
+            cdk::nuts::NotificationPayload::MintQuoteBolt11Response(quote_resp) => {
+                NotificationPayload::MintQuoteUpdate {
+                    quote: quote_resp.into(),
+                }
+            }
+            cdk::nuts::NotificationPayload::MeltQuoteBolt11Response(quote_resp) => {
+                NotificationPayload::MeltQuoteUpdate {
+                    quote: quote_resp.into(),
+                }
+            }
+            _ => NotificationPayload::ProofState {
+                proof_states: vec![],
+            },
+        }
+    }
+}

+ 278 - 0
crates/cdk-wasm/src/types/transaction.rs

@@ -0,0 +1,278 @@
+//! Transaction-related WASM types
+
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use super::amount::{Amount, CurrencyUnit};
+use super::keys::PublicKey;
+use super::mint::MintUrl;
+use super::proof::Proofs;
+use super::quote::PaymentMethod;
+use crate::error::WasmError;
+
+/// WASM-compatible Transaction
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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>,
+    /// Payment request (e.g., BOLT11 invoice, BOLT12 offer)
+    pub payment_request: Option<String>,
+    /// Payment proof (e.g., preimage for Lightning melt transactions)
+    pub payment_proof: Option<String>,
+    /// Payment method (e.g., Bolt11, Bolt12) for mint/melt transactions
+    pub payment_method: Option<PaymentMethod>,
+    /// Saga ID if this transaction was part of a saga
+    pub saga_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,
+            payment_request: tx.payment_request,
+            payment_proof: tx.payment_proof,
+            payment_method: tx.payment_method.map(Into::into),
+            saga_id: tx.saga_id.map(|id| id.to_string()),
+        }
+    }
+}
+
+/// Convert WASM Transaction to CDK Transaction
+impl TryFrom<Transaction> for cdk::wallet::types::Transaction {
+    type Error = WasmError;
+
+    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,
+            payment_request: tx.payment_request,
+            payment_proof: tx.payment_proof,
+            payment_method: tx.payment_method.map(Into::into),
+            saga_id: tx.saga_id.and_then(|id| Uuid::from_str(&id).ok()),
+        })
+    }
+}
+
+impl Transaction {
+    /// Convert Transaction to JSON string
+    pub fn to_json(&self) -> Result<String, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode Transaction from JSON string
+pub fn decode_transaction(json: String) -> Result<Transaction, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode Transaction to JSON string
+pub fn encode_transaction(transaction: Transaction) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&transaction)?)
+}
+
+/// Check if a transaction matches the given filter conditions
+pub fn transaction_matches_conditions(
+    transaction: &Transaction,
+    mint_url: Option<MintUrl>,
+    direction: Option<TransactionDirection>,
+    unit: Option<CurrencyUnit>,
+) -> Result<bool, WasmError> {
+    let cdk_transaction: cdk::wallet::types::Transaction = transaction.clone().try_into()?;
+    let cdk_mint_url = mint_url.map(|url| url.try_into()).transpose()?;
+    let cdk_direction = direction.map(Into::into);
+    let cdk_unit = unit.map(Into::into);
+    Ok(cdk_transaction.matches_conditions(&cdk_mint_url, &cdk_direction, &cdk_unit))
+}
+
+/// WASM-compatible TransactionDirection
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+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,
+        }
+    }
+}
+
+/// WASM-compatible TransactionId
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[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, WasmError> {
+        // Validate hex string length (should be 64 characters for 32 bytes)
+        if hex.len() != 64 {
+            return Err(WasmError::internal(
+                "Transaction ID hex must be exactly 64 characters (32 bytes)",
+            ));
+        }
+
+        // Validate hex format
+        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
+            return Err(WasmError::internal(
+                "Transaction ID hex contains invalid characters",
+            ));
+        }
+
+        Ok(Self { hex })
+    }
+
+    /// Create from proofs
+    pub fn from_proofs(proofs: &Proofs) -> Result<Self, WasmError> {
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.iter().map(|p| p.clone().try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+        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 = WasmError;
+
+    fn try_from(id: TransactionId) -> Result<Self, Self::Error> {
+        cdk::wallet::types::TransactionId::from_hex(&id.hex)
+            .map_err(|e| WasmError::internal(format!("Invalid transaction ID: {}", e)))
+    }
+}
+
+/// WASM-compatible AuthProof
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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 = WasmError;
+
+    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| WasmError::internal(format!("Invalid keyset ID: {}", e)))?,
+            secret: {
+                use std::str::FromStr;
+                cdk::secret::Secret::from_str(&auth_proof.secret)
+                    .map_err(|e| WasmError::internal(format!("Invalid secret: {}", e)))?
+            },
+            c: cdk::nuts::PublicKey::from_str(&auth_proof.c)
+                .map_err(|e| WasmError::internal(format!("Invalid public key: {}", e)))?,
+            dleq: None, // WASM doesn't expose DLEQ proofs for simplicity
+        })
+    }
+}
+
+impl AuthProof {
+    /// Convert AuthProof to JSON string
+    pub fn to_json(&self) -> Result<String, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode AuthProof from JSON string
+pub fn decode_auth_proof(json: String) -> Result<AuthProof, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode AuthProof to JSON string
+pub fn encode_auth_proof(proof: AuthProof) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&proof)?)
+}

+ 675 - 0
crates/cdk-wasm/src/types/wallet.rs

@@ -0,0 +1,675 @@
+//! Wallet-related WASM types
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+use super::amount::{Amount, SplitTarget};
+use super::proof::{Proofs, SpendingConditions};
+use crate::error::WasmError;
+use crate::token::Token;
+
+/// WASM-compatible SendMemo
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode SendMemo from JSON string
+pub fn decode_send_memo(json: String) -> Result<SendMemo, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode SendMemo to JSON string
+pub fn encode_send_memo(memo: SendMemo) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&memo)?)
+}
+
+/// WASM-compatible SendKind
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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(),
+            },
+        }
+    }
+}
+
+/// WASM-compatible Send options
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode SendOptions from JSON string
+pub fn decode_send_options(json: String) -> Result<SendOptions, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode SendOptions to JSON string
+pub fn encode_send_options(options: SendOptions) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&options)?)
+}
+
+/// WASM-compatible SecretKey
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[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, WasmError> {
+        // Validate hex string length (should be 64 characters for 32 bytes)
+        if hex.len() != 64 {
+            return Err(WasmError::internal(
+                "Secret key hex must be exactly 64 characters (32 bytes)",
+            ));
+        }
+
+        // Validate hex format
+        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
+            return Err(WasmError::internal(
+                "Secret key hex contains invalid characters",
+            ));
+        }
+
+        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(),
+        }
+    }
+}
+
+/// WASM-compatible Receive options
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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, WasmError> {
+        Ok(serde_json::to_string(self)?)
+    }
+}
+
+/// Decode ReceiveOptions from JSON string
+pub fn decode_receive_options(json: String) -> Result<ReceiveOptions, WasmError> {
+    Ok(serde_json::from_str(&json)?)
+}
+
+/// Encode ReceiveOptions to JSON string
+pub fn encode_receive_options(options: ReceiveOptions) -> Result<String, WasmError> {
+    Ok(serde_json::to_string(&options)?)
+}
+
+/// WASM-compatible PreparedSend
+///
+/// This wraps the data from a prepared send operation along with a reference
+/// to the wallet. The actual PreparedSend<'a> from cdk has a lifetime parameter
+/// that doesn't work with WASM, so we store the wallet and cached data separately.
+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 {
+    /// Create a new WASM PreparedSend from a cdk::wallet::PreparedSend and wallet
+    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(),
+        }
+    }
+}
+
+impl PreparedSend {
+    /// Get the operation ID for this prepared send
+    pub fn operation_id(&self) -> String {
+        self.operation_id.to_string()
+    }
+
+    /// Get the amount to send
+    pub fn amount(&self) -> Amount {
+        self.amount
+    }
+
+    /// Get the proofs that will be used
+    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
+    }
+
+    /// Get the total fee for this send operation
+    pub fn fee(&self) -> Amount {
+        Amount::new(self.swap_fee.value + self.send_fee.value)
+    }
+
+    /// Confirm the prepared send and create a token
+    pub async fn confirm(
+        self: std::sync::Arc<Self>,
+        memo: Option<String>,
+    ) -> Result<Token, WasmError> {
+        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())
+    }
+
+    /// Cancel the prepared send operation
+    pub async fn cancel(self: std::sync::Arc<Self>) -> Result<(), WasmError> {
+        self.wallet
+            .cancel_send(
+                self.operation_id,
+                self.proofs_to_swap.clone(),
+                self.proofs_to_send.clone(),
+            )
+            .await?;
+        Ok(())
+    }
+}
+
+/// WASM-compatible FinalizedMelt result
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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(),
+        }
+    }
+}
+
+/// WASM-compatible PreparedMelt
+///
+/// This wraps the data from a prepared melt operation along with a reference
+/// to the wallet. The actual PreparedMelt<'a> from cdk has a lifetime parameter
+/// that doesn't work with WASM, so we store the wallet and cached data separately.
+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 {
+    /// Create a new WASM PreparedMelt from a cdk::wallet::PreparedMelt and wallet
+    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(),
+        }
+    }
+}
+
+impl PreparedMelt {
+    /// Get the operation ID for this prepared melt
+    pub fn operation_id(&self) -> String {
+        self.operation_id.to_string()
+    }
+
+    /// Get the quote ID
+    pub fn quote_id(&self) -> String {
+        self.quote.id.clone()
+    }
+
+    /// Get the amount to be melted
+    pub fn amount(&self) -> Amount {
+        self.quote.amount.into()
+    }
+
+    /// Get the fee reserve from the quote
+    pub fn fee_reserve(&self) -> Amount {
+        self.quote.fee_reserve.into()
+    }
+
+    /// Get the swap fee
+    pub fn swap_fee(&self) -> Amount {
+        self.swap_fee
+    }
+
+    /// Get the input fee
+    pub fn input_fee(&self) -> Amount {
+        self.input_fee
+    }
+
+    /// Get the total fee (swap fee + input fee)
+    pub fn total_fee(&self) -> Amount {
+        Amount::new(self.swap_fee.value + self.input_fee.value)
+    }
+
+    /// Returns true if a swap would be performed (proofs_to_swap is not empty)
+    pub fn requires_swap(&self) -> bool {
+        !self.proofs_to_swap.is_empty()
+    }
+
+    /// Get the total fee if swap is performed (current default behavior)
+    pub fn total_fee_with_swap(&self) -> Amount {
+        Amount::new(self.swap_fee.value + self.input_fee.value)
+    }
+
+    /// Get the input fee if swap is skipped (fee on all proofs sent directly)
+    pub fn input_fee_without_swap(&self) -> Amount {
+        self.input_fee_without_swap
+    }
+
+    /// Get the fee savings from skipping the 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)
+        }
+    }
+
+    /// Get the expected change amount if swap is skipped
+    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))
+    }
+
+    /// Get the proofs that will be used
+    pub fn proofs(&self) -> Proofs {
+        self.proofs.iter().cloned().map(|p| p.into()).collect()
+    }
+
+    /// Confirm the prepared melt and execute the payment
+    pub async fn confirm(&self) -> Result<FinalizedMelt, WasmError> {
+        self.confirm_with_options(MeltConfirmOptions::default())
+            .await
+    }
+
+    /// Confirm the prepared melt with custom options
+    pub async fn confirm_with_options(
+        &self,
+        options: MeltConfirmOptions,
+    ) -> Result<FinalizedMelt, WasmError> {
+        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())
+    }
+
+    /// Cancel the prepared melt and release reserved proofs
+    pub async fn cancel(&self) -> Result<(), WasmError> {
+        self.wallet
+            .cancel_prepared_melt(
+                self.operation_id,
+                self.proofs.clone(),
+                self.proofs_to_swap.clone(),
+            )
+            .await?;
+        Ok(())
+    }
+}
+
+/// WASM-compatible MeltOptions
+#[derive(Debug, Clone, Serialize, Deserialize)]
+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(),
+            },
+        }
+    }
+}
+
+/// Restored Data
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Restored {
+    pub spent: Amount,
+    pub unspent: Amount,
+    pub pending: Amount,
+}
+
+impl From<cdk::wallet::Restored> for Restored {
+    fn from(restored: cdk::wallet::Restored) -> Self {
+        Self {
+            spent: restored.spent.into(),
+            unspent: restored.unspent.into(),
+            pending: restored.pending.into(),
+        }
+    }
+}
+
+/// WASM-compatible options for confirming a melt operation
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct MeltConfirmOptions {
+    /// Skip the pre-melt swap and send proofs directly to melt.
+    /// When true, saves swap input fees but gets change from melt instead.
+    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,
+        }
+    }
+}

+ 382 - 0
crates/cdk-wasm/src/wallet.rs

@@ -0,0 +1,382 @@
+//! WASM Wallet bindings
+
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cdk::wallet::{Wallet as CdkWallet, WalletBuilder as CdkWalletBuilder};
+use wasm_bindgen::prelude::*;
+
+use crate::error::WasmError;
+use crate::local_storage::LocalStorageDatabase;
+use crate::types::*;
+
+/// WASM-compatible Wallet
+#[wasm_bindgen]
+pub struct Wallet {
+    #[allow(missing_debug_implementations)]
+    inner: Arc<CdkWallet>,
+}
+
+impl std::fmt::Debug for Wallet {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Wallet")
+            .field("mint_url", &self.inner.mint_url)
+            .finish()
+    }
+}
+
+impl Wallet {
+    /// Create a Wallet from an existing CDK wallet (internal use only)
+    pub(crate) fn from_inner(inner: Arc<CdkWallet>) -> Self {
+        Self { inner }
+    }
+}
+
+#[wasm_bindgen]
+impl Wallet {
+    /// Create a new Wallet
+    #[wasm_bindgen(constructor)]
+    pub fn new(
+        mint_url: String,
+        unit: JsValue,
+        mnemonic: String,
+        db: JsValue,
+        target_proof_count: Option<u32>,
+    ) -> Result<Wallet, WasmError> {
+        let unit: CurrencyUnit =
+            serde_wasm_bindgen::from_value(unit).map_err(WasmError::internal)?;
+
+        let m = Mnemonic::parse(&mnemonic)
+            .map_err(|e| WasmError::internal(format!("Invalid mnemonic: {}", e)))?;
+        let seed = m.to_seed_normalized("");
+
+        // Use provided JS database or fall back to in-memory store
+        // TODO: Accept a JS database implementation via WalletDatabaseBridge
+        let _ = db;
+        let localstore = LocalStorageDatabase::new().into_arc();
+
+        let wallet = CdkWalletBuilder::new()
+            .mint_url(mint_url.parse().map_err(|e: cdk::mint_url::Error| {
+                WasmError::internal(format!("Invalid URL: {}", e))
+            })?)
+            .unit(unit.into())
+            .seed(seed)
+            .localstore(localstore)
+            .target_proof_count(target_proof_count.unwrap_or(3) as usize)
+            .build()
+            .map_err(WasmError::from)?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Get the mint URL
+    #[wasm_bindgen(js_name = "mintUrl")]
+    pub fn mint_url(&self) -> JsValue {
+        let url: MintUrl = self.inner.mint_url.clone().into();
+        serde_wasm_bindgen::to_value(&url).unwrap_or(JsValue::NULL)
+    }
+
+    /// Get the currency unit
+    pub fn unit(&self) -> JsValue {
+        let unit: CurrencyUnit = self.inner.unit.clone().into();
+        serde_wasm_bindgen::to_value(&unit).unwrap_or(JsValue::NULL)
+    }
+
+    /// Set metadata cache TTL in seconds
+    #[wasm_bindgen(js_name = "setMetadataCacheTtl")]
+    pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
+        let ttl = ttl_secs.map(std::time::Duration::from_secs);
+        self.inner.set_metadata_cache_ttl(ttl);
+    }
+
+    /// Get total balance
+    #[wasm_bindgen(js_name = "totalBalance")]
+    pub async fn total_balance(&self) -> Result<Amount, WasmError> {
+        let balance = self.inner.total_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Get total pending balance
+    #[wasm_bindgen(js_name = "totalPendingBalance")]
+    pub async fn total_pending_balance(&self) -> Result<Amount, WasmError> {
+        let balance = self.inner.total_pending_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Get total reserved balance
+    #[wasm_bindgen(js_name = "totalReservedBalance")]
+    pub async fn total_reserved_balance(&self) -> Result<Amount, WasmError> {
+        let balance = self.inner.total_reserved_balance().await?;
+        Ok(balance.into())
+    }
+
+    /// Fetch mint info from mint
+    #[wasm_bindgen(js_name = "fetchMintInfo")]
+    pub async fn fetch_mint_info(&self) -> Result<JsValue, WasmError> {
+        let info = self.inner.fetch_mint_info().await?;
+        let wasm_info: Option<MintInfo> = info.map(Into::into);
+        serde_wasm_bindgen::to_value(&wasm_info).map_err(WasmError::internal)
+    }
+
+    /// Load mint info (from cache if fresh)
+    #[wasm_bindgen(js_name = "loadMintInfo")]
+    pub async fn load_mint_info(&self) -> Result<JsValue, WasmError> {
+        let info = self.inner.load_mint_info().await?;
+        let wasm_info: MintInfo = info.into();
+        serde_wasm_bindgen::to_value(&wasm_info).map_err(WasmError::internal)
+    }
+
+    /// Receive tokens
+    pub async fn receive(
+        &self,
+        token_str: String,
+        options: JsValue,
+    ) -> Result<Amount, WasmError> {
+        let opts: ReceiveOptions =
+            serde_wasm_bindgen::from_value(options).map_err(WasmError::internal)?;
+        let amount = self.inner.receive(&token_str, opts.into()).await?;
+        Ok(amount.into())
+    }
+
+    /// Restore wallet from seed
+    pub async fn restore(&self) -> Result<JsValue, WasmError> {
+        let restored = self.inner.restore().await?;
+        let wasm_restored: Restored = restored.into();
+        serde_wasm_bindgen::to_value(&wasm_restored).map_err(WasmError::internal)
+    }
+
+    /// Get a mint quote
+    #[wasm_bindgen(js_name = "mintQuote")]
+    pub async fn mint_quote(
+        &self,
+        payment_method: JsValue,
+        amount: Option<Amount>,
+        description: Option<String>,
+        extra: Option<String>,
+    ) -> Result<JsValue, WasmError> {
+        let method: PaymentMethod =
+            serde_wasm_bindgen::from_value(payment_method).map_err(WasmError::internal)?;
+        let quote = self
+            .inner
+            .mint_quote(method, amount.map(Into::into), description, extra)
+            .await?;
+        let wasm_quote: MintQuote = quote.into();
+        serde_wasm_bindgen::to_value(&wasm_quote).map_err(WasmError::internal)
+    }
+
+    /// Mint tokens
+    pub async fn mint(
+        &self,
+        quote_id: String,
+        amount_split_target: JsValue,
+        spending_conditions: JsValue,
+    ) -> Result<JsValue, WasmError> {
+        let split: SplitTarget =
+            serde_wasm_bindgen::from_value(amount_split_target).map_err(WasmError::internal)?;
+        let conditions: Option<SpendingConditions> = if spending_conditions.is_null()
+            || spending_conditions.is_undefined()
+        {
+            None
+        } else {
+            Some(
+                serde_wasm_bindgen::from_value(spending_conditions).map_err(WasmError::internal)?,
+            )
+        };
+
+        let cdk_conditions = conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let proofs = self
+            .inner
+            .mint(&quote_id, split.into(), cdk_conditions)
+            .await?;
+        let wasm_proofs: Proofs = proofs.into_iter().map(|p| p.into()).collect();
+        serde_wasm_bindgen::to_value(&wasm_proofs).map_err(WasmError::internal)
+    }
+
+    /// Get a melt quote
+    #[wasm_bindgen(js_name = "meltQuote")]
+    pub async fn melt_quote(
+        &self,
+        method: JsValue,
+        request: String,
+        options: JsValue,
+        extra: Option<String>,
+    ) -> Result<JsValue, WasmError> {
+        let payment_method: PaymentMethod =
+            serde_wasm_bindgen::from_value(method).map_err(WasmError::internal)?;
+        let cdk_options: Option<MeltOptions> = if options.is_null() || options.is_undefined() {
+            None
+        } else {
+            Some(serde_wasm_bindgen::from_value(options).map_err(WasmError::internal)?)
+        };
+
+        let quote = self
+            .inner
+            .melt_quote::<cdk::nuts::PaymentMethod, _>(
+                payment_method.into(),
+                request,
+                cdk_options.map(Into::into),
+                extra,
+            )
+            .await?;
+        let wasm_quote: MeltQuote = quote.into();
+        serde_wasm_bindgen::to_value(&wasm_quote).map_err(WasmError::internal)
+    }
+
+    /// Swap proofs
+    pub async fn swap(
+        &self,
+        amount: JsValue,
+        amount_split_target: JsValue,
+        input_proofs: JsValue,
+        spending_conditions: JsValue,
+        include_fees: bool,
+    ) -> Result<JsValue, WasmError> {
+        let amt: Option<Amount> = if amount.is_null() || amount.is_undefined() {
+            None
+        } else {
+            Some(serde_wasm_bindgen::from_value(amount).map_err(WasmError::internal)?)
+        };
+        let split: SplitTarget =
+            serde_wasm_bindgen::from_value(amount_split_target).map_err(WasmError::internal)?;
+        let proofs: Proofs =
+            serde_wasm_bindgen::from_value(input_proofs).map_err(WasmError::internal)?;
+        let conditions: Option<SpendingConditions> = if spending_conditions.is_null()
+            || spending_conditions.is_undefined()
+        {
+            None
+        } else {
+            Some(
+                serde_wasm_bindgen::from_value(spending_conditions).map_err(WasmError::internal)?,
+            )
+        };
+
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+        let cdk_conditions = conditions.map(|sc| sc.try_into()).transpose()?;
+
+        let result = self
+            .inner
+            .swap(
+                amt.map(Into::into),
+                split.into(),
+                cdk_proofs,
+                cdk_conditions,
+                include_fees,
+            )
+            .await?;
+
+        let wasm_result: Option<Proofs> =
+            result.map(|proofs| proofs.into_iter().map(|p| p.into()).collect());
+        serde_wasm_bindgen::to_value(&wasm_result).map_err(WasmError::internal)
+    }
+
+    /// List transactions
+    #[wasm_bindgen(js_name = "listTransactions")]
+    pub async fn list_transactions(&self, direction: JsValue) -> Result<JsValue, WasmError> {
+        let dir: Option<TransactionDirection> = if direction.is_null() || direction.is_undefined() {
+            None
+        } else {
+            Some(serde_wasm_bindgen::from_value(direction).map_err(WasmError::internal)?)
+        };
+        let cdk_direction = dir.map(Into::into);
+        let transactions = self.inner.list_transactions(cdk_direction).await?;
+        let wasm_txs: Vec<Transaction> = transactions.into_iter().map(Into::into).collect();
+        serde_wasm_bindgen::to_value(&wasm_txs).map_err(WasmError::internal)
+    }
+
+    /// Check proofs spent status
+    #[wasm_bindgen(js_name = "checkProofsSpent")]
+    pub async fn check_proofs_spent(&self, proofs: JsValue) -> Result<Vec<u8>, WasmError> {
+        let wasm_proofs: Proofs =
+            serde_wasm_bindgen::from_value(proofs).map_err(WasmError::internal)?;
+        let cdk_proofs: Result<Vec<cdk::nuts::Proof>, _> =
+            wasm_proofs.into_iter().map(|p| p.try_into()).collect();
+        let cdk_proofs = cdk_proofs?;
+
+        let proof_states = self.inner.check_proofs_spent(cdk_proofs).await?;
+        Ok(proof_states
+            .into_iter()
+            .map(|ps| {
+                matches!(
+                    ps.state,
+                    cdk::nuts::State::Spent | cdk::nuts::State::PendingSpent
+                ) as u8
+            })
+            .collect())
+    }
+
+    /// Refresh keysets from the mint
+    #[wasm_bindgen(js_name = "refreshKeysets")]
+    pub async fn refresh_keysets(&self) -> Result<JsValue, WasmError> {
+        let keysets = self.inner.refresh_keysets().await?;
+        let wasm_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
+        serde_wasm_bindgen::to_value(&wasm_keysets).map_err(WasmError::internal)
+    }
+
+    /// Get the active keyset for the wallet's unit
+    #[wasm_bindgen(js_name = "getActiveKeyset")]
+    pub async fn get_active_keyset(&self) -> Result<JsValue, WasmError> {
+        let keyset = self.inner.get_active_keyset().await?;
+        let wasm_keyset: KeySetInfo = keyset.into();
+        serde_wasm_bindgen::to_value(&wasm_keyset).map_err(WasmError::internal)
+    }
+
+    /// Check all pending proofs
+    #[wasm_bindgen(js_name = "checkAllPendingProofs")]
+    pub async fn check_all_pending_proofs(&self) -> Result<Amount, WasmError> {
+        let amount = self.inner.check_all_pending_proofs().await?;
+        Ok(amount.into())
+    }
+
+    /// Set Clear Auth Token (CAT)
+    #[wasm_bindgen(js_name = "setCat")]
+    pub async fn set_cat(&self, cat: String) -> Result<(), WasmError> {
+        self.inner.set_cat(cat).await?;
+        Ok(())
+    }
+
+    /// Set refresh token
+    #[wasm_bindgen(js_name = "setRefreshToken")]
+    pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), WasmError> {
+        self.inner.set_refresh_token(refresh_token).await?;
+        Ok(())
+    }
+
+    /// Refresh access token
+    #[wasm_bindgen(js_name = "refreshAccessToken")]
+    pub async fn refresh_access_token(&self) -> Result<(), WasmError> {
+        self.inner.refresh_access_token().await?;
+        Ok(())
+    }
+
+    /// Refresh the status of a mint quote from the mint
+    #[wasm_bindgen(js_name = "refreshMintQuoteStatus")]
+    pub async fn refresh_mint_quote_status(
+        &self,
+        quote_id: String,
+    ) -> Result<JsValue, WasmError> {
+        let quote = self.inner.refresh_mint_quote_status(&quote_id).await?;
+        let wasm_quote: MintQuote = quote.into();
+        serde_wasm_bindgen::to_value(&wasm_quote).map_err(WasmError::internal)
+    }
+}
+
+/// Generates a new random mnemonic phrase
+#[wasm_bindgen(js_name = "generateMnemonic")]
+pub fn generate_mnemonic() -> Result<String, WasmError> {
+    let mnemonic = Mnemonic::generate(12)
+        .map_err(|e| WasmError::internal(format!("Failed to generate mnemonic: {}", e)))?;
+    Ok(mnemonic.to_string())
+}
+
+/// Converts a mnemonic phrase to its entropy bytes
+#[wasm_bindgen(js_name = "mnemonicToEntropy")]
+pub fn mnemonic_to_entropy(mnemonic: String) -> Result<Vec<u8>, WasmError> {
+    let m = Mnemonic::parse(&mnemonic)
+        .map_err(|e| WasmError::internal(format!("Invalid mnemonic: {}", e)))?;
+    Ok(m.to_entropy())
+}

+ 203 - 0
crates/cdk-wasm/src/wallet_repository.rs

@@ -0,0 +1,203 @@
+//! WASM WalletRepository bindings
+
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cdk::wallet::wallet_repository::{
+    WalletRepository as CdkWalletRepository, WalletRepositoryBuilder,
+};
+use wasm_bindgen::prelude::*;
+
+use crate::error::WasmError;
+use crate::types::*;
+
+/// WASM-compatible WalletRepository
+#[wasm_bindgen]
+#[derive(Debug)]
+pub struct WalletRepository {
+    inner: Arc<CdkWalletRepository>,
+}
+
+#[wasm_bindgen]
+impl WalletRepository {
+    /// Create a new WalletRepository from mnemonic
+    pub async fn new(
+        mnemonic: String,
+        db: JsValue,
+    ) -> Result<WalletRepository, WasmError> {
+        let m = Mnemonic::parse(&mnemonic)
+            .map_err(|e| WasmError::internal(format!("Invalid mnemonic: {}", e)))?;
+        let seed = m.to_seed_normalized("");
+
+        // TODO: Accept a JS database implementation
+        let _ = db;
+        let localstore = crate::local_storage::LocalStorageDatabase::new().into_arc();
+
+        let wallet = WalletRepositoryBuilder::new()
+            .localstore(localstore)
+            .seed(seed)
+            .build()
+            .await?;
+
+        Ok(Self {
+            inner: Arc::new(wallet),
+        })
+    }
+
+    /// Set metadata cache TTL in seconds for a specific mint
+    #[wasm_bindgen(js_name = "setMetadataCacheTtlForMint")]
+    pub async fn set_metadata_cache_ttl_for_mint(
+        &self,
+        mint_url: String,
+        ttl_secs: Option<u64>,
+    ) -> Result<(), WasmError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
+            .parse()
+            .map_err(|e: cdk::mint_url::Error| WasmError::internal(format!("Invalid URL: {}", e)))?;
+        let wallets = self.inner.get_wallets().await;
+
+        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
+            let ttl = ttl_secs.map(std::time::Duration::from_secs);
+            wallet.set_metadata_cache_ttl(ttl);
+            Ok(())
+        } else {
+            Err(WasmError::internal(format!(
+                "Mint not found: {}",
+                cdk_mint_url
+            )))
+        }
+    }
+
+    /// Set metadata cache TTL in seconds for all mints
+    #[wasm_bindgen(js_name = "setMetadataCacheTtlForAllMints")]
+    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
+        let wallets = self.inner.get_wallets().await;
+        let ttl = ttl_secs.map(std::time::Duration::from_secs);
+
+        for wallet in wallets.iter() {
+            wallet.set_metadata_cache_ttl(ttl);
+        }
+    }
+
+    /// Add a mint to this WalletRepository
+    #[wasm_bindgen(js_name = "addMint")]
+    pub async fn add_mint(
+        &self,
+        mint_url: String,
+        unit: JsValue,
+        target_proof_count: Option<u32>,
+    ) -> Result<(), WasmError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
+            .parse()
+            .map_err(|e: cdk::mint_url::Error| WasmError::internal(format!("Invalid URL: {}", e)))?;
+
+        let config = target_proof_count.map(|count| {
+            cdk::wallet::wallet_repository::WalletConfig::new()
+                .with_target_proof_count(count as usize)
+        });
+
+        let unit_enum: CurrencyUnit = if unit.is_null() || unit.is_undefined() {
+            CurrencyUnit::Sat
+        } else {
+            serde_wasm_bindgen::from_value(unit).map_err(WasmError::internal)?
+        };
+
+        self.inner
+            .create_wallet(cdk_mint_url, unit_enum.into(), config)
+            .await?;
+
+        Ok(())
+    }
+
+    /// Remove mint from WalletRepository
+    #[wasm_bindgen(js_name = "removeMint")]
+    pub async fn remove_mint(
+        &self,
+        mint_url: String,
+        currency_unit: JsValue,
+    ) -> Result<(), WasmError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
+            .parse()
+            .map_err(|e: cdk::mint_url::Error| WasmError::internal(format!("Invalid URL: {}", e)))?;
+
+        let unit: CurrencyUnit =
+            serde_wasm_bindgen::from_value(currency_unit).map_err(WasmError::internal)?;
+
+        self.inner
+            .remove_wallet(cdk_mint_url, unit.into())
+            .await
+            .map_err(|e| e.into())
+    }
+
+    /// Check if mint is in wallet
+    #[wasm_bindgen(js_name = "hasMint")]
+    pub async fn has_mint(&self, mint_url: String) -> bool {
+        if let Ok(cdk_mint_url) = mint_url.parse::<cdk::mint_url::MintUrl>() {
+            self.inner.has_mint(&cdk_mint_url).await
+        } else {
+            false
+        }
+    }
+
+    /// Get wallet balances for all mints
+    #[wasm_bindgen(js_name = "getBalances")]
+    pub async fn get_balances(&self) -> Result<JsValue, WasmError> {
+        let balances = self.inner.get_balances().await?;
+        let balance_map: std::collections::HashMap<String, Amount> = balances
+            .into_iter()
+            .map(|(mint_url, amount)| (mint_url.to_string(), amount.into()))
+            .collect();
+        serde_wasm_bindgen::to_value(&balance_map).map_err(WasmError::internal)
+    }
+
+    /// Get all wallets from WalletRepository
+    #[wasm_bindgen(js_name = "getWallets")]
+    pub async fn get_wallets(&self) -> Vec<crate::wallet::Wallet> {
+        let wallets = self.inner.get_wallets().await;
+        wallets
+            .into_iter()
+            .map(|w| crate::wallet::Wallet::from_inner(Arc::new(w)))
+            .collect()
+    }
+
+    /// Get a specific wallet by mint URL and unit
+    #[wasm_bindgen(js_name = "getWallet")]
+    pub async fn get_wallet(
+        &self,
+        mint_url: String,
+        unit: JsValue,
+    ) -> Result<crate::wallet::Wallet, WasmError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url
+            .parse()
+            .map_err(|e: cdk::mint_url::Error| WasmError::internal(format!("Invalid URL: {}", e)))?;
+        let unit_enum: CurrencyUnit =
+            serde_wasm_bindgen::from_value(unit).map_err(WasmError::internal)?;
+        let unit_cdk: cdk::nuts::CurrencyUnit = unit_enum.into();
+        let wallet = self.inner.get_wallet(&cdk_mint_url, &unit_cdk).await?;
+        Ok(crate::wallet::Wallet::from_inner(Arc::new(wallet)))
+    }
+}
+
+/// Token data type for WASM
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct TokenData {
+    pub mint_url: MintUrl,
+    pub proofs: Proofs,
+    pub memo: Option<String>,
+    pub value: Amount,
+    pub unit: CurrencyUnit,
+    pub redeem_fee: Option<Amount>,
+}
+
+impl From<cdk::wallet::TokenData> for TokenData {
+    fn from(data: cdk::wallet::TokenData) -> Self {
+        Self {
+            mint_url: data.mint_url.into(),
+            proofs: data.proofs.into_iter().map(Into::into).collect(),
+            memo: data.memo,
+            value: data.value.into(),
+            unit: data.unit.into(),
+            redeem_fee: data.redeem_fee.map(Into::into),
+        }
+    }
+}

+ 170 - 0
crates/cdk-wasm/www/index.html

@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>cdk-wasm Demo</title>
+  <style>
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    body {
+      font-family: system-ui, -apple-system, sans-serif;
+      background: #0f0f0f;
+      color: #e0e0e0;
+      padding: 2rem;
+      max-width: 720px;
+      margin: 0 auto;
+    }
+    h1 { color: #f7931a; margin-bottom: 1.5rem; }
+    .card {
+      background: #1a1a1a;
+      border: 1px solid #333;
+      border-radius: 8px;
+      padding: 1.25rem;
+      margin-bottom: 1rem;
+    }
+    .card h2 {
+      font-size: 0.85rem;
+      text-transform: uppercase;
+      letter-spacing: 0.05em;
+      color: #888;
+      margin-bottom: 0.75rem;
+    }
+    .status {
+      padding: 0.5rem 0.75rem;
+      border-radius: 4px;
+      font-size: 0.9rem;
+      margin-bottom: 0.5rem;
+    }
+    .status.ok { background: #0d2818; color: #4ade80; }
+    .status.err { background: #2d0f0f; color: #f87171; }
+    .status.info { background: #1a1a2e; color: #93c5fd; }
+    .status.loading { background: #1a1a1a; color: #fbbf24; }
+    .mnemonic {
+      font-family: 'SF Mono', 'Fira Code', monospace;
+      font-size: 1.1rem;
+      word-spacing: 0.3em;
+      line-height: 1.8;
+      color: #f7931a;
+    }
+    .detail { color: #aaa; font-size: 0.9rem; line-height: 1.6; }
+    .detail strong { color: #e0e0e0; }
+    pre {
+      background: #111;
+      padding: 0.75rem;
+      border-radius: 4px;
+      overflow-x: auto;
+      font-size: 0.8rem;
+      color: #aaa;
+      margin-top: 0.5rem;
+    }
+  </style>
+</head>
+<body>
+  <h1>cdk-wasm Demo</h1>
+
+  <div class="card">
+    <h2>WASM Module</h2>
+    <div id="wasm-status" class="status loading">Loading WASM module...</div>
+  </div>
+
+  <div class="card">
+    <h2>Mnemonic</h2>
+    <div id="mnemonic-status" class="status info">Waiting...</div>
+    <div id="mnemonic" class="mnemonic"></div>
+  </div>
+
+  <div class="card">
+    <h2>Wallet</h2>
+    <div id="wallet-status" class="status info">Waiting...</div>
+    <div id="wallet-detail" class="detail"></div>
+  </div>
+
+  <div class="card">
+    <h2>Balance</h2>
+    <div id="balance-status" class="status info">Waiting...</div>
+  </div>
+
+  <div class="card">
+    <h2>Mint Info</h2>
+    <div id="mintinfo-status" class="status info">Waiting...</div>
+    <pre id="mintinfo-detail" style="display:none"></pre>
+  </div>
+
+  <script type="module">
+    import init, { initDefaultLogging, generateMnemonic, Wallet } from '../pkg/cdk_wasm.js';
+
+    const $ = id => document.getElementById(id);
+    function setStatus(id, cls, text) {
+      const el = $(id);
+      el.className = 'status ' + cls;
+      el.textContent = text;
+    }
+
+    try {
+      await init();
+      setStatus('wasm-status', 'ok', 'WASM module loaded');
+    } catch (e) {
+      setStatus('wasm-status', 'err', 'Failed to load WASM: ' + e.message);
+      throw e;
+    }
+
+    try {
+      initDefaultLogging();
+    } catch (_) {}
+
+    // Generate mnemonic
+    let mnemonic;
+    try {
+      mnemonic = generateMnemonic();
+      setStatus('mnemonic-status', 'ok', 'Generated 12-word mnemonic');
+      $('mnemonic').textContent = mnemonic;
+    } catch (e) {
+      setStatus('mnemonic-status', 'err', 'Failed: ' + e.message);
+      throw e;
+    }
+
+    // Create wallet
+    const mintUrl = 'https://testnut.cashu.space';
+    let wallet;
+    try {
+      setStatus('wallet-status', 'loading', 'Creating wallet...');
+      wallet = new Wallet(mintUrl, 'Sat', mnemonic, null, 3);
+      setStatus('wallet-status', 'ok', 'Wallet created');
+      $('wallet-detail').innerHTML =
+        `<strong>Mint:</strong> ${mintUrl}<br><strong>Unit:</strong> sat`;
+    } catch (e) {
+      setStatus('wallet-status', 'err', 'Failed: ' + (e.message || e));
+      $('wallet-detail').textContent = String(e);
+    }
+
+    // Fetch balance
+    if (wallet) {
+      try {
+        setStatus('balance-status', 'loading', 'Fetching balance...');
+        const balance = await wallet.totalBalance();
+        setStatus('balance-status', 'ok', 'Balance: ' + balance.value + ' sat');
+      } catch (e) {
+        setStatus('balance-status', 'err', 'Balance error: ' + (e.message || e));
+      }
+    }
+
+    // Fetch mint info
+    if (wallet) {
+      try {
+        setStatus('mintinfo-status', 'loading', 'Fetching mint info...');
+        const info = await wallet.fetchMintInfo();
+        if (info) {
+          setStatus('mintinfo-status', 'ok', 'Mint info retrieved');
+          const detail = $('mintinfo-detail');
+          detail.style.display = 'block';
+          detail.textContent = JSON.stringify(info, null, 2);
+        } else {
+          setStatus('mintinfo-status', 'info', 'Mint returned no info');
+        }
+      } catch (e) {
+        setStatus('mintinfo-status', 'err', 'Mint info error: ' + (e.message || e));
+      }
+    }
+  </script>
+</body>
+</html>

+ 263 - 0
crates/cdk-wasm/www/mint.html

@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>cdk-wasm Mint Example</title>
+  <style>
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    body {
+      font-family: system-ui, -apple-system, sans-serif;
+      background: #0f0f0f;
+      color: #e0e0e0;
+      padding: 2rem;
+      max-width: 720px;
+      margin: 0 auto;
+    }
+    h1 { color: #f7931a; margin-bottom: 0.5rem; }
+    p.subtitle { color: #888; margin-bottom: 1.5rem; font-size: 0.9rem; }
+    .card {
+      background: #1a1a1a;
+      border: 1px solid #333;
+      border-radius: 8px;
+      padding: 1.25rem;
+      margin-bottom: 1rem;
+    }
+    .card h2 {
+      font-size: 0.85rem;
+      text-transform: uppercase;
+      letter-spacing: 0.05em;
+      color: #888;
+      margin-bottom: 0.75rem;
+    }
+    .status {
+      padding: 0.5rem 0.75rem;
+      border-radius: 4px;
+      font-size: 0.9rem;
+      margin-bottom: 0.5rem;
+    }
+    .status.ok { background: #0d2818; color: #4ade80; }
+    .status.err { background: #2d0f0f; color: #f87171; }
+    .status.info { background: #1a1a2e; color: #93c5fd; }
+    .status.loading { background: #1a1a1a; color: #fbbf24; }
+    .detail { color: #aaa; font-size: 0.9rem; line-height: 1.6; }
+    .detail strong { color: #e0e0e0; }
+    pre {
+      background: #111;
+      padding: 0.75rem;
+      border-radius: 4px;
+      overflow-x: auto;
+      font-size: 0.8rem;
+      color: #aaa;
+      margin-top: 0.5rem;
+      word-break: break-all;
+      white-space: pre-wrap;
+    }
+    input, button {
+      font-family: inherit;
+      font-size: 0.9rem;
+      padding: 0.5rem 0.75rem;
+      border-radius: 4px;
+      border: 1px solid #444;
+      background: #222;
+      color: #e0e0e0;
+    }
+    input { width: 100%; margin-bottom: 0.5rem; }
+    button {
+      background: #f7931a;
+      color: #000;
+      border: none;
+      cursor: pointer;
+      font-weight: 600;
+    }
+    button:disabled { opacity: 0.4; cursor: not-allowed; }
+    button:hover:not(:disabled) { background: #ffa940; }
+    .row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
+    .row input { margin-bottom: 0; }
+    #log {
+      max-height: 300px;
+      overflow-y: auto;
+      font-size: 0.8rem;
+      color: #aaa;
+      background: #111;
+      padding: 0.75rem;
+      border-radius: 4px;
+      margin-top: 0.5rem;
+    }
+    #log div { margin-bottom: 0.25rem; }
+    #log .ts { color: #666; }
+    #log .event { color: #93c5fd; }
+    #log .ok { color: #4ade80; }
+    #log .err { color: #f87171; }
+  </style>
+</head>
+<body>
+  <h1>Mint &amp; Subscribe</h1>
+  <p class="subtitle">Create a mint quote, pay the invoice, and watch the status update in real-time.</p>
+
+  <div class="card">
+    <h2>Setup</h2>
+    <div id="setup-status" class="status loading">Loading WASM...</div>
+    <div class="detail">
+      <div class="row">
+        <input id="mint-url" type="text" value="https://testnut.cashu.space" placeholder="Mint URL">
+      </div>
+      <div class="row">
+        <input id="amount" type="number" value="21" placeholder="Amount (sats)" min="1">
+        <button id="btn-mint" disabled>Create Mint Quote</button>
+      </div>
+    </div>
+  </div>
+
+  <div class="card">
+    <h2>Mint Quote</h2>
+    <div id="quote-status" class="status info">Waiting...</div>
+    <pre id="quote-detail" style="display:none"></pre>
+  </div>
+
+  <div class="card">
+    <h2>Invoice</h2>
+    <div id="invoice-status" class="status info">Waiting for quote...</div>
+    <pre id="invoice" style="display:none"></pre>
+  </div>
+
+  <div class="card">
+    <h2>Subscription Log</h2>
+    <div id="sub-status" class="status info">Waiting for quote...</div>
+    <div id="log"></div>
+  </div>
+
+  <div class="card">
+    <h2>Minted Tokens</h2>
+    <div id="mint-status" class="status info">Waiting for payment...</div>
+    <pre id="mint-result" style="display:none"></pre>
+  </div>
+
+  <div class="card">
+    <h2>Balance</h2>
+    <div id="balance-status" class="status info">Waiting...</div>
+  </div>
+
+  <script type="module">
+    import init, { initDefaultLogging, generateMnemonic, Wallet } from '../pkg/cdk_wasm.js';
+
+    // ---- helpers ----------------------------------------------------------
+    const $ = id => document.getElementById(id);
+    function setStatus(id, cls, text) {
+      const el = $(id);
+      el.className = 'status ' + cls;
+      el.textContent = text;
+    }
+    function log(msg, cls = '') {
+      const el = $('log');
+      const ts = new Date().toLocaleTimeString();
+      el.innerHTML += `<div><span class="ts">[${ts}]</span> <span class="${cls}">${msg}</span></div>`;
+      el.scrollTop = el.scrollHeight;
+    }
+
+    // ---- init WASM --------------------------------------------------------
+    try {
+      await init();
+      setStatus('setup-status', 'ok', 'WASM loaded');
+    } catch (e) {
+      setStatus('setup-status', 'err', 'Failed: ' + e.message);
+      throw e;
+    }
+    try { initDefaultLogging(); } catch (_) {}
+
+    // ---- state ------------------------------------------------------------
+    let wallet = null;
+    let currentQuote = null;
+    let polling = false;
+
+    // ---- create quote -----------------------------------------------------
+    $('btn-mint').disabled = false;
+    $('btn-mint').addEventListener('click', async () => {
+      const mintUrl = $('mint-url').value.trim();
+      const amount = parseInt($('amount').value, 10);
+      if (!mintUrl || !amount || amount <= 0) return;
+
+      $('btn-mint').disabled = true;
+      polling = false; // stop any previous poll
+
+      try {
+        // Create wallet
+        setStatus('quote-status', 'loading', 'Creating wallet...');
+        const mnemonic = generateMnemonic();
+        wallet = new Wallet(mintUrl, 'Sat', mnemonic, null, 3);
+        log('Wallet created for ' + mintUrl);
+
+        // Create mint quote
+        setStatus('quote-status', 'loading', 'Requesting mint quote...');
+        currentQuote = await wallet.mintQuote('Bolt11', { value: amount }, null, null);
+        setStatus('quote-status', 'ok', `Quote ${currentQuote.id} created`);
+        $('quote-detail').style.display = 'block';
+        $('quote-detail').textContent = JSON.stringify(currentQuote, null, 2);
+        log(`Quote created: ${currentQuote.id}`, 'event');
+
+        // Show invoice
+        $('invoice').style.display = 'block';
+        $('invoice').textContent = currentQuote.request;
+        setStatus('invoice-status', 'ok', 'Pay this Lightning invoice to continue');
+
+        // Start polling
+        startPolling(currentQuote.id);
+      } catch (e) {
+        setStatus('quote-status', 'err', 'Error: ' + (e.message || e));
+        log('Error: ' + (e.message || e), 'err');
+        $('btn-mint').disabled = false;
+      }
+    });
+
+    // ---- poll for payment -------------------------------------------------
+    async function startPolling(quoteId) {
+      polling = true;
+      setStatus('sub-status', 'loading', 'Polling quote status every 3s...');
+      log('Started polling for quote ' + quoteId, 'event');
+
+      while (polling) {
+        await new Promise(r => setTimeout(r, 3000));
+        if (!polling) break;
+
+        try {
+          const updated = await wallet.refreshMintQuoteStatus(quoteId);
+          const state = updated.state;
+          log(`Status: ${state}`);
+
+          if (state === 'Paid' || state === 'Issued') {
+            polling = false;
+            setStatus('sub-status', 'ok', `Quote is ${state}!`);
+            log(`Invoice paid! State: ${state}`, 'ok');
+            await mintTokens(quoteId);
+            return;
+          }
+        } catch (e) {
+          log('Poll error: ' + (e.message || e), 'err');
+        }
+      }
+    }
+
+    // ---- mint tokens ------------------------------------------------------
+    async function mintTokens(quoteId) {
+      try {
+        setStatus('mint-status', 'loading', 'Minting tokens...');
+        log('Minting tokens...', 'event');
+        const proofs = await wallet.mint(quoteId, 'Default', null);
+        setStatus('mint-status', 'ok', `Minted ${Array.isArray(proofs) ? proofs.length : '?'} proofs`);
+        $('mint-result').style.display = 'block';
+        $('mint-result').textContent = JSON.stringify(proofs, null, 2);
+        log(`Minted ${Array.isArray(proofs) ? proofs.length : '?'} proofs`, 'ok');
+
+        // Show balance
+        const balance = await wallet.totalBalance();
+        setStatus('balance-status', 'ok', `Balance: ${balance.value} sat`);
+        log(`Balance: ${balance.value} sat`, 'ok');
+      } catch (e) {
+        setStatus('mint-status', 'err', 'Mint error: ' + (e.message || e));
+        log('Mint error: ' + (e.message || e), 'err');
+      }
+      $('btn-mint').disabled = false;
+    }
+  </script>
+</body>
+</html>

+ 0 - 2
crates/cdk/Cargo.toml

@@ -44,7 +44,6 @@ bitcoin.workspace = true
 ciborium.workspace = true
 lightning.workspace = true
 lightning-invoice.workspace = true
-regex.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_with.workspace = true
@@ -92,7 +91,6 @@ tls-api-native-tls = { version = "0.9", optional = true }
 tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
 cdk-signatory = { workspace = true, default-features = false }
 getrandom = { version = "0.2", features = ["js"] }
-ring = { version = "0.17.14", features = ["wasm32_unknown_unknown_js"] }
 rustls = { workspace = true, optional = true }
 
 uuid = { workspace = true, features = ["js"] }

+ 3 - 1
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -1,7 +1,9 @@
 //! HTTP Transport trait with a default implementation
 use std::fmt::Debug;
 
-use cdk_common::{AuthToken, HttpClient, HttpClientBuilder};
+#[cfg(not(target_arch = "wasm32"))]
+use cdk_common::HttpClientBuilder;
+use cdk_common::{AuthToken, HttpClient};
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 use hickory_resolver::config::ResolverConfig;
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]

+ 19 - 0
justfile

@@ -468,6 +468,25 @@ run-examples:
   cargo r --example proof_selection
   cargo r --example wallet
 
+# Build cdk-wasm package for browser use
+wasm-build *ARGS="--dev":
+  #!/usr/bin/env bash
+  set -euo pipefail
+  if [ ! -f Cargo.toml ]; then
+    cd {{invocation_directory()}}
+  fi
+  wasm-pack build crates/cdk-wasm --target web {{ARGS}}
+
+# Build cdk-wasm and serve the demo page at http://localhost:8080/www/
+wasm-serve PORT="8080": wasm-build
+  #!/usr/bin/env bash
+  set -euo pipefail
+  if [ ! -f Cargo.toml ]; then
+    cd {{invocation_directory()}}
+  fi
+  echo "Serving cdk-wasm demo at http://localhost:{{PORT}}/www/"
+  cd crates/cdk-wasm && python3 -m http.server {{PORT}}
+
 check-wasm *ARGS="--target wasm32-unknown-unknown":
   #!/usr/bin/env bash
   set -euo pipefail