diff --git a/Cargo.lock b/Cargo.lock index 94244d201..42cef20eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f9997f8650dd818369931b5672a18dbef95324d0513aa99aae758de8ce86e5b" +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + [[package]] name = "bitcoin-private" version = "0.1.0" @@ -449,6 +455,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals 0.2.0", + "hex-conservative", +] + [[package]] name = "bitcoincore-rpc" version = "0.16.0" @@ -757,12 +773,57 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + [[package]] name = "data-encoding" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "difflib" version = "0.4.0" @@ -863,7 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cb1f7f2489cce83bc3bd92784f9ba5271eeb6e729b975895fc541f78cbfcdca" dependencies = [ "bitcoin 0.30.2", - "bitcoin-internals", + "bitcoin-internals 0.1.0", "log", "reqwest", "serde", @@ -1559,7 +1620,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap", + "indexmap 2.2.2", "slab", "tokio", "tokio-util", @@ -1586,6 +1647,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1763,6 +1830,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -1843,6 +1916,17 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.2" @@ -1851,6 +1935,7 @@ checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -1899,6 +1984,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -2328,6 +2422,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "moksha-core" +version = "0.2.0-beta" +source = "git+https://github.com/ngutech21/moksha?rev=18d99977965662d46ccec29fecdb0ce493745917#18d99977965662d46ccec29fecdb0ce493745917" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bitcoin_hashes 0.13.0", + "getrandom", + "hex", + "itertools 0.12.1", + "rand", + "secp256k1 0.28.2", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "utoipa", + "uuid", +] + [[package]] name = "musig2" version = "0.1.0" @@ -2383,6 +2499,7 @@ dependencies = [ "lnurl-rs", "log", "mockall", + "moksha-core", "nostr", "nostr-sdk", "payjoin", @@ -2422,6 +2539,7 @@ dependencies = [ "lightning-invoice 0.29.0", "lnurl-rs", "log", + "moksha-core", "mutiny-core", "nostr", "once_cell", @@ -2579,6 +2697,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.17" @@ -2893,6 +3017,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3362,7 +3492,9 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ + "rand", "secp256k1-sys 0.9.2", + "serde", ] [[package]] @@ -3490,7 +3622,7 @@ version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ - "indexmap", + "indexmap 2.2.2", "itoa", "ryu", "serde", @@ -3508,6 +3640,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.2", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -3623,6 +3785,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.24.1" @@ -3752,6 +3920,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3918,7 +4117,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap", + "indexmap 2.2.2", "toml_datetime", "winnow", ] @@ -4119,6 +4318,30 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utoipa" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" +dependencies = [ + "indexmap 2.2.2", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "uuid" version = "1.7.0" @@ -4126,6 +4349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 194283ef8..eb408222b 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -56,6 +56,7 @@ fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", rev = "5a fedimint-ln-client = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } fedimint-bip39 = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } fedimint-ln-common = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } +moksha-core = { git = "https://github.com/ngutech21/moksha", rev = "18d99977965662d46ccec29fecdb0ce493745917" } base64 = "0.13.0" pbkdf2 = "0.11" diff --git a/mutiny-core/src/cashu.rs b/mutiny-core/src/cashu.rs new file mode 100644 index 000000000..c539bb0c1 --- /dev/null +++ b/mutiny-core/src/cashu.rs @@ -0,0 +1,85 @@ +use crate::error::MutinyError; +use moksha_core::primitives::PostMeltBolt11Response; +use moksha_core::primitives::{ + CashuErrorResponse, PostMeltBolt11Request, PostMeltQuoteBolt11Request, + PostMeltQuoteBolt11Response, +}; +use reqwest::Client; +use reqwest::StatusCode; +use serde_json::{json, Value}; +use url::Url; + +#[derive(Clone)] +pub struct CashuHttpClient { + client: Client, +} + +impl CashuHttpClient { + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + pub async fn post_melt_quote_bolt11( + &self, + url: &Url, + melt_quote_request: PostMeltQuoteBolt11Request, + ) -> Result { + self.mint_post( + &url.join("/v1/melt/quote/bolt11")?, + json!(melt_quote_request), + ) + .await + } + + pub async fn post_melt_bolt11( + &self, + url: &Url, + melt_request: PostMeltBolt11Request, + ) -> Result { + self.mint_post(&url.join("/v1/melt/bolt11")?, json!(melt_request)) + .await + } + + async fn mint_post( + &self, + url: &Url, + body: Value, + ) -> Result { + let res = self + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(body.to_string()) + .send() + .await + .map_err(|_| MutinyError::CashuMintError)?; + Self::parse_cashu_mint_response(res).await + } + + async fn parse_cashu_mint_response( + res: reqwest::Response, + ) -> Result { + match res.status() { + StatusCode::OK => { + let response_text = res.text().await.map_err(|_| MutinyError::CashuMintError)?; + match serde_json::from_str::(&response_text) { + Ok(data) => Ok(data), + Err(_) => Err(MutinyError::CashuMintError), + } + } + _ => { + let txt = res.text().await.map_err(|_| MutinyError::CashuMintError)?; + let data = serde_json::from_str::(&txt) + .map_err(|_| MutinyError::CashuMintError)?; + + match data.code { + // error code in nutshell for tokens that have been spent + 11001 => Err(MutinyError::TokenAlreadySpent), + _ => Err(MutinyError::CashuMintError), + } + } + } + } +} diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 5676887a5..50b1eee6e 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -162,6 +162,15 @@ pub enum MutinyError { /// Payjoin configuration error #[error("Payjoin configuration failed.")] PayjoinConfigError, + /// Error calling Cashu Mint + #[error("Error calling Cashu Mint.")] + CashuMintError, + /// Mint URL in token was empty + #[error("Mint URL in token is empty.")] + EmptyMintURLError, + /// Token already spent. + #[error("Token has been already spent.")] + TokenAlreadySpent, #[error(transparent)] Other(#[from] anyhow::Error), } @@ -241,6 +250,9 @@ impl PartialEq for MutinyError { (Self::NostrError, Self::NostrError) => true, (Self::IncorrectPassword, Self::IncorrectPassword) => true, (Self::SamePassword, Self::SamePassword) => true, + (Self::CashuMintError, Self::CashuMintError) => true, + (Self::EmptyMintURLError, Self::EmptyMintURLError) => true, + (Self::TokenAlreadySpent, Self::TokenAlreadySpent) => true, (Self::Other(e), Self::Other(e2)) => e.to_string() == e2.to_string(), _ => false, } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 79ba11c6f..69954ec4d 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -10,6 +10,7 @@ extern crate core; pub mod auth; +mod cashu; mod chain; pub mod encrypt; pub mod error; @@ -40,6 +41,7 @@ pub mod vss; #[cfg(test)] mod test_utils; +use crate::cashu::CashuHttpClient; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; @@ -93,6 +95,11 @@ use lightning::util::logger::Logger; use lightning::{log_debug, log_error, log_info, log_trace, log_warn}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response}; +use moksha_core::primitives::{ + CurrencyUnit, PostMeltBolt11Request, PostMeltBolt11Response, PostMeltQuoteBolt11Request, + PostMeltQuoteBolt11Response, +}; +use moksha_core::token::TokenV3; use nostr_sdk::{Client, RelayPoolNotification}; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; @@ -114,6 +121,7 @@ use mockall::{automock, predicate::*}; const DEFAULT_PAYMENT_TIMEOUT: u64 = 30; const MAX_FEDERATION_INVOICE_AMT: u64 = 200_000; const SWAP_LABEL: &str = "SWAP"; +const MELT_CASHU_TOKEN: &str = "Cashu Token Melt"; #[cfg_attr(test, automock)] pub trait InvoiceHandler { @@ -856,6 +864,7 @@ impl MutinyWalletBuilder { network, skip_hodl_invoices: self.skip_hodl_invoices, safe_mode: self.safe_mode, + cashu_client: CashuHttpClient::new(), }; // if we are in safe mode, don't create any nodes or @@ -911,6 +920,7 @@ pub struct MutinyWallet { network: Network, skip_hodl_invoices: bool, safe_mode: bool, + cashu_client: CashuHttpClient, } impl MutinyWallet { @@ -2349,6 +2359,75 @@ impl MutinyWallet { pub fn is_safe_mode(&self) -> bool { self.safe_mode } + + /// Calls upon a Cashu mint and redeems/melts the token. + pub async fn melt_cashu_token( + &self, + token_v3: TokenV3, + ) -> Result, MutinyError> { + let mut invoices: Vec = Vec::with_capacity(token_v3.tokens.len()); + + for token in token_v3.tokens { + let mint_url = match token.mint { + Some(url) => url, + None => return Err(MutinyError::EmptyMintURLError), + }; + + let total_proofs_amount = token.proofs.total_amount(); + let mut invoice_pct = 0.99; + // create invoice for 1% less than proofs amount + let mut invoice_amount = total_proofs_amount as f64 * invoice_pct; + let mut mutiny_invoice: MutinyInvoice; + let mut mutiny_invoice_str: Bolt11Invoice; + let mut melt_quote_res: PostMeltQuoteBolt11Response; + + loop { + mutiny_invoice = self + .create_invoice(invoice_amount as u64, vec![MELT_CASHU_TOKEN.to_string()]) + .await?; + + mutiny_invoice_str = mutiny_invoice + .bolt11 + .clone() + .expect("The invoice should have BOLT11"); + + let quote_request = PostMeltQuoteBolt11Request { + request: mutiny_invoice_str.to_string(), + unit: CurrencyUnit::Sat, + }; + + melt_quote_res = self + .cashu_client + .post_melt_quote_bolt11(&mint_url, quote_request) + .await?; + + if melt_quote_res.amount + melt_quote_res.fee_reserve > total_proofs_amount { + // if invoice created was too big, lower amount + invoice_pct -= 0.01; + invoice_amount *= invoice_pct; + } else { + break; + } + } + + let melt_request = PostMeltBolt11Request { + quote: melt_quote_res.quote, + inputs: token.proofs, + outputs: vec![], + }; + + let post_melt_bolt11_response: PostMeltBolt11Response = self + .cashu_client + .post_melt_bolt11(&mint_url, melt_request) + .await?; + + if post_melt_bolt11_response.paid { + mutiny_invoice = self.get_invoice(&mutiny_invoice_str).await?; + invoices.push(mutiny_invoice); + } + } + Ok(invoices) + } } impl InvoiceHandler for MutinyWallet { diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index e418ebf2c..eaf38304a 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -45,6 +45,7 @@ once_cell = "1.18.0" hex-conservative = "0.1.1" payjoin = { version = "0.13.0", features = ["send", "base64"] } fedimint-core = { git = "https://github.com/fedimint/fedimint", rev = "5ade2536015a12a7e003a42b159ccc4a431e1a32" } +moksha-core = { git = "https://github.com/ngutech21/moksha", rev = "18d99977965662d46ccec29fecdb0ce493745917" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index bc02daf28..8f7955af6 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -159,6 +159,15 @@ pub enum MutinyJsError { /// Payjoin configuration error #[error("Payjoin configuration failed.")] PayjoinConfigError, + /// Error calling Cashu Mint + #[error("Error calling Cashu Mint")] + CashuMintError, + /// Mint URL in token was empty + #[error("Mint URL in token is empty")] + EmptyMintURLError, + /// Token already spent. + #[error("Token has been already spent.")] + TokenAlreadySpent, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -208,6 +217,9 @@ impl From for MutinyJsError { MutinyError::BitcoinPriceError => MutinyJsError::BitcoinPriceError, MutinyError::IncorrectPassword => MutinyJsError::IncorrectPassword, MutinyError::SamePassword => MutinyJsError::SamePassword, + MutinyError::CashuMintError => MutinyJsError::CashuMintError, + MutinyError::EmptyMintURLError => MutinyJsError::EmptyMintURLError, + MutinyError::TokenAlreadySpent => MutinyJsError::TokenAlreadySpent, MutinyError::Other(e) => { error!("Got unhandled error: {e}"); // FIXME: For some unknown reason, InsufficientBalance is being returned as `Other` @@ -290,6 +302,12 @@ impl From for MutinyJsError { } } +impl From for MutinyJsError { + fn from(e: moksha_core::error::MokshaCoreError) -> Self { + e.into() + } +} + impl From for JsValue { fn from(e: MutinyJsError) -> Self { JsValue::from(e.to_string()) diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index e892e4841..f55b169ad 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -30,6 +30,7 @@ use lightning::{log_error, log_info, log_warn, routing::gossip::NodeId, util::lo use lightning_invoice::Bolt11Invoice; use lnurl::lightning_address::LightningAddress; use lnurl::lnurl::LnUrl; +use moksha_core::token::TokenV3; use mutiny_core::auth::MutinyAuthClient; use mutiny_core::lnurlauth::AuthManager; use mutiny_core::nostr::nip49::NIP49URI; @@ -906,6 +907,18 @@ impl MutinyWallet { Ok(self.inner.lnurl_withdraw(&lnurl, amount_sats).await?) } + /// Calls upon a Cash mint and melts the token from it. + #[wasm_bindgen] + pub async fn melt_cashu_token( + &self, + maybe_token: String, + ) -> Result */, MutinyJsError> { + let token = TokenV3::deserialize(maybe_token)?; + let result = self.inner.melt_cashu_token(token).await?; + let invoices: Vec = result.into_iter().map(|i| i.into()).collect(); + Ok(JsValue::from_serde(&invoices)?) + } + /// Authenticates with a LNURL-auth for the given profile. #[wasm_bindgen] pub async fn lnurl_auth(&self, lnurl: String) -> Result<(), MutinyJsError> {