diff --git a/Cargo.lock b/Cargo.lock index d4e1201..f228d57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,18 +80,18 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.27.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "secp256k1-sys", ] [[package]] name = "secp256k1-sys" -version = "0.8.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" dependencies = [ "cc", ] @@ -191,7 +191,7 @@ dependencies = [ [[package]] name = "webln" -version = "0.1.0" +version = "0.2.0" dependencies = [ "js-sys", "secp256k1", diff --git a/webln-js/Cargo.toml b/webln-js/Cargo.toml index 3a4691c..cf9d088 100644 --- a/webln-js/Cargo.toml +++ b/webln-js/Cargo.toml @@ -19,7 +19,7 @@ default = [] [dependencies] console_error_panic_hook = { version = "0.1", optional = true } js-sys.workspace = true -webln = { version = "0.1", path = "../webln", default-features = false } +webln = { version = "0.2", path = "../webln", default-features = false } wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true diff --git a/webln-js/src/lib.rs b/webln-js/src/lib.rs index 87c7560..1594227 100644 --- a/webln-js/src/lib.rs +++ b/webln-js/src/lib.rs @@ -8,8 +8,11 @@ extern crate alloc; +use alloc::string::String; +use alloc::vec::Vec; use core::ops::Deref; +use send_payment::JsSendMultiPaymentResponse; use wasm_bindgen::prelude::*; use webln::WebLN; @@ -115,6 +118,20 @@ impl JsWebLN { .into()) } + /// Request that the user sends multiple payments. + #[wasm_bindgen(js_name = sendMultiPayment)] + pub async fn send_multi_payment( + &self, + invoices: Vec, + ) -> Result { + Ok(self + .inner + .send_multi_payment(invoices) + .await + .map_err(into_err)? + .into()) + } + /// Request that the user sends a payment for an invoice. /// The payment will only be initiated and will not wait for a preimage to be returned. /// This is useful when paying HOLD Invoices. There is no guarantee that the payment will be successfully sent to the receiver. diff --git a/webln-js/src/request_invoice.rs b/webln-js/src/request_invoice.rs index 86a6e9d..0c53dba 100644 --- a/webln-js/src/request_invoice.rs +++ b/webln-js/src/request_invoice.rs @@ -73,8 +73,8 @@ impl From for JsRequestInvoiceResponse { #[wasm_bindgen(js_class = RequestInvoiceResponse)] impl JsRequestInvoiceResponse { - #[wasm_bindgen(getter)] - pub fn invoice(&self) -> String { - self.inner.invoice.clone() + #[wasm_bindgen(getter, js_name = paymentRequest)] + pub fn payment_request(&self) -> String { + self.inner.payment_request.clone() } } diff --git a/webln-js/src/send_payment.rs b/webln-js/src/send_payment.rs index e9f86e7..84321ad 100644 --- a/webln-js/src/send_payment.rs +++ b/webln-js/src/send_payment.rs @@ -2,9 +2,12 @@ // Distributed under the MIT software license use alloc::string::String; +use alloc::vec::Vec; use wasm_bindgen::prelude::*; -use webln::SendPaymentResponse; +use webln::{ + SendMultiPaymentError, SendMultiPaymentResponse, SendMultiPaymentSingle, SendPaymentResponse, +}; #[wasm_bindgen(js_name = SendPaymentResponse)] pub struct JsSendPaymentResponse { @@ -24,3 +27,85 @@ impl JsSendPaymentResponse { self.inner.preimage.clone() } } + +#[wasm_bindgen(js_name = SendMultiPaymentSingle)] +pub struct JsSendMultiPaymentSingle { + inner: SendMultiPaymentSingle, +} + +#[wasm_bindgen(js_class = SendMultiPaymentSingle)] +impl JsSendMultiPaymentSingle { + #[wasm_bindgen(getter, js_name = paymentRequest)] + pub fn payment_request(&self) -> String { + self.inner.payment_request.clone() + } + + #[wasm_bindgen(getter)] + pub fn response(&self) -> JsSendPaymentResponse { + self.inner.response.clone().into() + } +} + +impl From for JsSendMultiPaymentSingle { + fn from(inner: SendMultiPaymentSingle) -> Self { + Self { inner } + } +} + +#[wasm_bindgen(js_name = SendMultiPaymentError)] +pub struct JsSendMultiPaymentError { + inner: SendMultiPaymentError, +} + +#[wasm_bindgen(js_class = SendMultiPaymentError)] +impl JsSendMultiPaymentError { + #[wasm_bindgen(getter, js_name = paymentRequest)] + pub fn payment_request(&self) -> String { + self.inner.payment_request.clone() + } + + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { + self.inner.message.clone() + } +} + +impl From for JsSendMultiPaymentError { + fn from(inner: SendMultiPaymentError) -> Self { + Self { inner } + } +} + +#[wasm_bindgen(js_name = SendMultiPaymentResponse)] +pub struct JsSendMultiPaymentResponse { + inner: SendMultiPaymentResponse, +} + +impl From for JsSendMultiPaymentResponse { + fn from(inner: SendMultiPaymentResponse) -> Self { + Self { inner } + } +} + +#[wasm_bindgen(js_class = SendMultiPaymentResponse)] +impl JsSendMultiPaymentResponse { + #[wasm_bindgen(getter)] + pub fn payments(&self) -> Vec { + self.inner + .payments + .iter() + .cloned() + .map(|e| e.into()) + .collect() + } + + #[wasm_bindgen(getter)] + pub fn errors(&self) -> Vec { + self.inner + .errors + .iter() + .cloned() + .map(|e| e.into()) + .collect() + } +} diff --git a/webln/Cargo.toml b/webln/Cargo.toml index fd6aa3b..789b26c 100644 --- a/webln/Cargo.toml +++ b/webln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "webln" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "WebLN - Lightning Web Standard" authors.workspace = true @@ -17,7 +17,7 @@ std = ["secp256k1/std", "wasm-bindgen/std"] [dependencies] js-sys.workspace = true -secp256k1 = { version = "0.27", default-features = false } +secp256k1 = { version = "0.28", default-features = false } wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true web-sys = { version = "0.3", default-features = false, features = ["Window"] } diff --git a/webln/src/lib.rs b/webln/src/lib.rs index bc1fef5..9034ba9 100644 --- a/webln/src/lib.rs +++ b/webln/src/lib.rs @@ -31,6 +31,7 @@ const GET_INFO: &str = "getInfo"; const KEYSEND: &str = "keysend"; const MAKE_INVOICE: &str = "makeInvoice"; const SEND_PAYMENT: &str = "sendPayment"; +const SEND_MULTI_PAYMENT: &str = "sendMultiPayment"; const SEND_PAYMENT_ASYNC: &str = "sendPaymentAsync"; const SIGN_MESSAGE: &str = "signMessage"; const VERIFY_MESSAGE: &str = "verifyMessage"; @@ -90,6 +91,24 @@ impl From for Error { } } +/// Get value from object key +fn get_value_by_key(obj: &Object, key: &str) -> Result { + Reflect::get(obj, &JsValue::from_str(key)) + .map_err(|_| Error::ObjectKeyNotFound(key.to_string())) +} + +trait Deserialize: Sized { + fn deserialize(value: JsValue) -> Result; +} + +impl Deserialize for bool { + fn deserialize(value: JsValue) -> Result { + value + .as_bool() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a bool"))) + } +} + /// Get Info Node Response #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GetInfoNode { @@ -111,6 +130,7 @@ pub enum GetInfoMethod { Keysend, MakeInvoice, SendPayment, + SendMultiPayment, SendPaymentAsync, SignMessage, VerifyMessage, @@ -131,6 +151,7 @@ impl From<&str> for GetInfoMethod { KEYSEND => Self::Keysend, MAKE_INVOICE => Self::MakeInvoice, SEND_PAYMENT => Self::SendPayment, + SEND_MULTI_PAYMENT => Self::SendMultiPayment, SEND_PAYMENT_ASYNC => Self::SendPaymentAsync, SIGN_MESSAGE => Self::SignMessage, VERIFY_MESSAGE => Self::VerifyMessage, @@ -153,6 +174,7 @@ impl fmt::Display for GetInfoMethod { Self::Keysend => write!(f, "{KEYSEND}"), Self::MakeInvoice => write!(f, "{MAKE_INVOICE}"), Self::SendPayment => write!(f, "{SEND_PAYMENT}"), + Self::SendMultiPayment => write!(f, "{SEND_MULTI_PAYMENT}"), Self::SendPaymentAsync => write!(f, "{SEND_PAYMENT_ASYNC}"), Self::SignMessage => write!(f, "{SIGN_MESSAGE}"), Self::VerifyMessage => write!(f, "{VERIFY_MESSAGE}"), @@ -175,6 +197,36 @@ pub struct GetInfoResponse { pub methods: Vec, } +impl Deserialize for GetInfoResponse { + fn deserialize(value: JsValue) -> Result { + let get_info_obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + + let node_obj: Object = get_value_by_key(&get_info_obj, "node")? + .dyn_into() + .map_err(|_| Error::SomethingGoneWrong)?; + + // Extract data + let alias: Option = get_value_by_key(&node_obj, "alias")?.as_string(); + let pubkey: Option = get_value_by_key(&node_obj, "pubkey")?.as_string(); + let color: Option = get_value_by_key(&node_obj, "color")?.as_string(); + let methods_array: Array = get_value_by_key(&get_info_obj, "methods")?.into(); + let methods: Vec = methods_array + .into_iter() + .filter_map(|m| m.as_string()) + .map(|m| GetInfoMethod::from(m.as_str())) + .collect(); + + Ok(Self { + node: GetInfoNode { + alias, + pubkey, + color, + }, + methods, + }) + } +} + /// Keysend args #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeysendArgs { @@ -195,6 +247,84 @@ pub struct SendPaymentResponse { pub preimage: String, } +impl Deserialize for SendPaymentResponse { + fn deserialize(value: JsValue) -> Result { + let send_payment_obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + let preimage = get_value_by_key(&send_payment_obj, "preimage")? + .as_string() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [preimage]")))?; + Ok(Self { preimage }) + } +} + +/// Send Multi Payment Single response +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SendMultiPaymentSingle { + /// Payment request + pub payment_request: String, + /// Error message + pub response: SendPaymentResponse, +} + +/// Send Multi Payment Error +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SendMultiPaymentError { + /// Payment request + pub payment_request: String, + /// Error message + pub message: String, +} + +impl Deserialize for SendMultiPaymentError { + fn deserialize(value: JsValue) -> Result { + let error_obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + let payment_request = get_value_by_key(&error_obj, "paymentRequest")? + .as_string() + .ok_or_else(|| { + Error::TypeMismatch(String::from("expected a string [paymentRequest]")) + })?; + let message = get_value_by_key(&error_obj, "message")? + .as_string() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [message]")))?; + Ok(Self { + payment_request, + message, + }) + } +} + +/// Send Payment Response +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SendMultiPaymentResponse { + /// Payments + pub payments: Vec, + /// Errors + pub errors: Vec, +} + +impl Deserialize for SendMultiPaymentResponse { + fn deserialize(value: JsValue) -> Result { + let obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + + // let js_payments: Array = self + // .get_value_by_key(&obj, "payments")? + // .dyn_into()?; + let js_errors: Array = get_value_by_key(&obj, "errors")?.dyn_into()?; + + // Deserialize errors + let mut errors: Vec = + Vec::with_capacity(js_errors.length() as usize); + for error in js_errors.into_iter() { + errors.push(SendMultiPaymentError::deserialize(error)?); + } + + Ok(Self { + payments: Vec::new(), // TODO + errors, + }) + } +} + /// Request invoice args /// /// **All amounts are denominated in SAT.** @@ -303,7 +433,20 @@ impl TryFrom<&RequestInvoiceArgs> for Object { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RequestInvoiceResponse { /// BOLT-11 invoice - pub invoice: String, + pub payment_request: String, +} + +impl Deserialize for RequestInvoiceResponse { + fn deserialize(value: JsValue) -> Result { + let obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + Ok(Self { + payment_request: get_value_by_key(&obj, "paymentRequest")? + .as_string() + .ok_or_else(|| { + Error::TypeMismatch(String::from("expected a string [paymentRequest]")) + })?, + }) + } } /// Sign Message Response @@ -315,6 +458,19 @@ pub struct SignMessageResponse { pub signature: String, } +impl Deserialize for SignMessageResponse { + fn deserialize(value: JsValue) -> Result { + let obj: Object = value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + let message: String = get_value_by_key(&obj, "message")? + .as_string() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [message]")))?; + let signature: String = get_value_by_key(&obj, "signature")? + .as_string() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [signature]")))?; + Ok(Self { message, signature }) + } +} + /// Balance Response #[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct BalanceResponse { @@ -324,6 +480,19 @@ pub struct BalanceResponse { pub currency: Option, } +impl Deserialize for BalanceResponse { + fn deserialize(value: JsValue) -> Result { + let balance_response_obj: Object = + value.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; + let balance: f64 = get_value_by_key(&balance_response_obj, "balance")? + .as_f64() + .ok_or_else(|| Error::TypeMismatch(String::from("expected a number [balance]")))?; + let currency: Option = + get_value_by_key(&balance_response_obj, "currency")?.as_string(); + Ok(Self { balance, currency }) + } +} + /// WebLN instance #[derive(Debug, Clone)] pub struct WebLN { @@ -350,21 +519,13 @@ impl WebLN { .map_err(|_| Error::NamespaceNotFound(name.to_string())) } - /// Get value from object key - fn get_value_by_key(&self, obj: &Object, key: &str) -> Result { - Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|_| Error::ObjectKeyNotFound(key.to_string())) - } - /// Check if `webln` is enabled without explicitly enabling it through `webln.enable()` /// (which may cause a confirmation popup in some providers) pub async fn is_enabled(&self) -> Result { let func: Function = self.get_func(&self.webln_obj, IS_ENABLED)?; let promise: Promise = Promise::resolve(&func.call0(&self.webln_obj)?); let result: JsValue = JsFuture::from(promise).await?; - result - .as_bool() - .ok_or_else(|| Error::TypeMismatch(String::from("expected a bool"))) + bool::deserialize(result) } /// To begin interacting with WebLN APIs you'll first need to enable the provider. @@ -382,32 +543,7 @@ impl WebLN { let func: Function = self.get_func(&self.webln_obj, GET_INFO)?; let promise: Promise = Promise::resolve(&func.call0(&self.webln_obj)?); let result: JsValue = JsFuture::from(promise).await?; - let get_info_obj: Object = result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - - let node_obj: Object = self - .get_value_by_key(&get_info_obj, "node")? - .dyn_into() - .map_err(|_| Error::SomethingGoneWrong)?; - - // Extract data - let alias: Option = self.get_value_by_key(&node_obj, "alias")?.as_string(); - let pubkey: Option = self.get_value_by_key(&node_obj, "pubkey")?.as_string(); - let color: Option = self.get_value_by_key(&node_obj, "color")?.as_string(); - let methods_array: Array = self.get_value_by_key(&get_info_obj, "methods")?.into(); - let methods: Vec = methods_array - .into_iter() - .filter_map(|m| m.as_string()) - .map(|m| GetInfoMethod::from(m.as_str())) - .collect(); - - Ok(GetInfoResponse { - node: GetInfoNode { - alias, - pubkey, - color, - }, - methods, - }) + GetInfoResponse::deserialize(result) } /// Request the user to send a keysend payment. @@ -429,14 +565,7 @@ impl WebLN { let promise: Promise = Promise::resolve(&func.call1(&self.webln_obj, &keysend_obj.into())?); let result: JsValue = JsFuture::from(promise).await?; - let send_payment_obj: Object = result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - - Ok(SendPaymentResponse { - preimage: self - .get_value_by_key(&send_payment_obj, "preimage")? - .as_string() - .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [preimage]")))?, - }) + SendPaymentResponse::deserialize(result) } /// Request that the user creates an invoice to be used by the web app @@ -445,22 +574,11 @@ impl WebLN { args: &RequestInvoiceArgs, ) -> Result { let func: Function = self.get_func(&self.webln_obj, MAKE_INVOICE)?; - let request_invoice_obj: Object = args.try_into()?; - let promise: Promise = Promise::resolve(&func.call1(&self.webln_obj, &request_invoice_obj.into())?); let result: JsValue = JsFuture::from(promise).await?; - let request_invoice_response_obj: Object = - result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - Ok(RequestInvoiceResponse { - invoice: self - .get_value_by_key(&request_invoice_response_obj, "paymentRequest")? - .as_string() - .ok_or_else(|| { - Error::TypeMismatch(String::from("expected a string [paymentRequest]")) - })?, - }) + RequestInvoiceResponse::deserialize(result) } /// Request that the user sends a payment for an invoice. @@ -474,13 +592,26 @@ impl WebLN { let func: Function = self.get_func(&self.webln_obj, SEND_PAYMENT)?; let promise: Promise = Promise::resolve(&func.call1(&self.webln_obj, &invoice.into())?); let result: JsValue = JsFuture::from(promise).await?; - let send_payment_obj: Object = result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - Ok(SendPaymentResponse { - preimage: self - .get_value_by_key(&send_payment_obj, "preimage")? - .as_string() - .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [preimage]")))?, - }) + SendPaymentResponse::deserialize(result) + } + + /// Request that the user sends multiple payments. + pub async fn send_multi_payment( + &self, + invoices: I, + ) -> Result + where + I: IntoIterator, + S: AsRef, + { + let invoices: Array = invoices + .into_iter() + .map(|i| JsValue::from_str(i.as_ref())) + .collect(); + let func: Function = self.get_func(&self.webln_obj, SEND_MULTI_PAYMENT)?; + let promise: Promise = Promise::resolve(&func.call1(&self.webln_obj, &invoices.into())?); + let result: JsValue = JsFuture::from(promise).await?; + SendMultiPaymentResponse::deserialize(result) } /// Request that the user sends a payment for an invoice. @@ -510,19 +641,7 @@ impl WebLN { let func: Function = self.get_func(&self.webln_obj, SIGN_MESSAGE)?; let promise: Promise = Promise::resolve(&func.call1(&self.webln_obj, &message.into())?); let result: JsValue = JsFuture::from(promise).await?; - let sign_message_response_obj: Object = - result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - - // Extract data - let signature: String = self - .get_value_by_key(&sign_message_response_obj, "signature")? - .as_string() - .ok_or_else(|| Error::TypeMismatch(String::from("expected a string [signature]")))?; - - Ok(SignMessageResponse { - message: message.to_string(), - signature, - }) + SignMessageResponse::deserialize(result) } /// Fetch the balance of the current account. @@ -530,18 +649,6 @@ impl WebLN { let func: Function = self.get_func(&self.webln_obj, GET_BALANCE)?; let promise: Promise = Promise::resolve(&func.call0(&self.webln_obj)?); let result: JsValue = JsFuture::from(promise).await?; - let balance_response_obj: Object = - result.dyn_into().map_err(|_| Error::SomethingGoneWrong)?; - - // Extract data - let balance: f64 = self - .get_value_by_key(&balance_response_obj, "balance")? - .as_f64() - .ok_or_else(|| Error::TypeMismatch(String::from("expected a number [balance]")))?; - let currency: Option = self - .get_value_by_key(&balance_response_obj, "currency")? - .as_string(); - - Ok(BalanceResponse { balance, currency }) + BalanceResponse::deserialize(result) } }