diff --git a/Cargo.lock b/Cargo.lock index 1aa6c85..6c0026b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -33,6 +42,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + [[package]] name = "log" version = "0.4.20" @@ -63,6 +78,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "syn" version = "2.0.48" @@ -161,6 +194,7 @@ name = "webln" version = "0.1.0" dependencies = [ "js-sys", + "secp256k1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", diff --git a/webln-js/src/keysend.rs b/webln-js/src/keysend.rs new file mode 100644 index 0000000..b14f128 --- /dev/null +++ b/webln-js/src/keysend.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 Yuki Kishimoto +// Distributed under the MIT software license + +use std::ops::Deref; +use std::str::FromStr; + +use wasm_bindgen::prelude::*; +use webln::secp256k1::PublicKey; +use webln::KeysendArgs; + +use crate::error::{into_err, Result}; + +#[wasm_bindgen(js_name = KeysendArgs)] +pub struct JsKeysendArgs { + inner: KeysendArgs, +} + +impl Deref for JsKeysendArgs { + type Target = KeysendArgs; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[wasm_bindgen(js_class = KeysendArgs)] +impl JsKeysendArgs { + pub fn new(destination: String, amount: u32) -> Result { + let destination: PublicKey = PublicKey::from_str(&destination).map_err(into_err)?; + let amount: u64 = amount as u64; + Ok(Self { + inner: KeysendArgs { + destination, + amount, + }, + }) + } +} diff --git a/webln-js/src/lib.rs b/webln-js/src/lib.rs index 5dd0dcf..db85bc1 100644 --- a/webln-js/src/lib.rs +++ b/webln-js/src/lib.rs @@ -5,14 +5,20 @@ #![allow(non_snake_case)] #![allow(clippy::new_without_default)] -use get_info::JsGetInfoResponse; +use std::ops::Deref; + use wasm_bindgen::prelude::*; use webln::WebLN; pub mod error; pub mod get_info; +pub mod keysend; +pub mod send_payment; use self::error::{into_err, Result}; +use self::get_info::JsGetInfoResponse; +use self::keysend::JsKeysendArgs; +use self::send_payment::JsSendPaymentResponse; #[wasm_bindgen(start)] pub fn start() { @@ -54,4 +60,15 @@ impl JsWebLN { pub async fn get_info(&self) -> Result { Ok(self.inner.get_info().await.map_err(into_err)?.into()) } + + /// Request the user to send a keysend payment. + /// This is a spontaneous payment that does not require an invoice and only needs a destination public key and and amount. + pub async fn keysend(&self, args: &JsKeysendArgs) -> Result { + Ok(self + .inner + .keysend(args.deref()) + .await + .map_err(into_err)? + .into()) + } } diff --git a/webln-js/src/send_payment.rs b/webln-js/src/send_payment.rs new file mode 100644 index 0000000..47a154e --- /dev/null +++ b/webln-js/src/send_payment.rs @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Yuki Kishimoto +// Distributed under the MIT software license + +use wasm_bindgen::prelude::*; +use webln::SendPaymentResponse; + +#[wasm_bindgen(js_name = SendPaymentResponse)] +pub struct JsSendPaymentResponse { + inner: SendPaymentResponse, +} + +impl From for JsSendPaymentResponse { + fn from(inner: SendPaymentResponse) -> Self { + Self { inner } + } +} + +#[wasm_bindgen(js_class = SendPaymentResponse)] +impl JsSendPaymentResponse { + #[wasm_bindgen(getter)] + pub fn preimage(&self) -> String { + self.inner.preimage.clone() + } +} diff --git a/webln/Cargo.toml b/webln/Cargo.toml index 616cd9f..e4ea500 100644 --- a/webln/Cargo.toml +++ b/webln/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["webln", "lightning", "bitcoin"] [dependencies] js-sys.workspace = true -wasm-bindgen.workspace = true +secp256k1 = { version = "0.27", default-features = false, features = ["std"] } +wasm-bindgen = { workspace = true, features = ["std"]} 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 278634a..bb797f0 100644 --- a/webln/src/lib.rs +++ b/webln/src/lib.rs @@ -3,9 +3,12 @@ //! WebLN - Lightning Web Standard +pub extern crate secp256k1; + use core::fmt; use js_sys::{Array, Function, Object, Promise, Reflect}; +use secp256k1::PublicKey; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::Window; @@ -13,6 +16,7 @@ use web_sys::Window; pub const IS_ENABLED: &str = "isEnabled"; pub const ENABLE: &str = "enable"; pub const GET_INFO: &str = "getInfo"; +pub const KEYSEND: &str = "keysend"; /// WebLN error #[derive(Debug)] @@ -84,7 +88,7 @@ where IS_ENABLED => Self::IsEnabled, ENABLE => Self::Enable, GET_INFO => Self::GetInfo, - "keysend" => Self::Keysend, + KEYSEND => Self::Keysend, "makeInvoice" => Self::MakeInvoice, "sendPayment" => Self::SendPayment, "sendPaymentAsync" => Self::SendPaymentAsync, @@ -106,7 +110,7 @@ impl fmt::Display for GetInfoMethod { Self::IsEnabled => write!(f, "{IS_ENABLED}"), Self::Enable => write!(f, "{ENABLE}"), Self::GetInfo => write!(f, "{GET_INFO}"), - Self::Keysend => write!(f, "keysend"), + Self::Keysend => write!(f, "{KEYSEND}"), Self::MakeInvoice => write!(f, "makeInvoice"), Self::SendPayment => write!(f, "sendPayment"), Self::SendPaymentAsync => write!(f, "sendPaymentAsync"), @@ -128,6 +132,23 @@ pub struct GetInfoResponse { pub methods: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeysendArgs { + /// Public key of the destination node. + pub destination: PublicKey, + /// Amount in SAT + pub amount: u64, + // TODO: add TLVRegistry enum + // The key should be a stringified integer from the . + // The value should be an unencoded, plain string. + // pub custom: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SendPaymentResponse { + pub preimage: String, +} + /// WebLN instance #[derive(Debug, Clone)] pub struct WebLN { @@ -214,4 +235,33 @@ impl WebLN { methods, }) } + + /// Request the user to send a keysend payment. + /// This is a spontaneous payment that does not require an invoice and only needs a destination public key and and amount. + pub async fn keysend(&self, args: &KeysendArgs) -> Result { + let func: Function = self.get_func(&self.webln_obj, KEYSEND)?; + + let keysend_obj = Object::new(); + Reflect::set( + &keysend_obj, + &JsValue::from_str("destination"), + &args.destination.to_string().into(), + )?; + Reflect::set( + &keysend_obj, + &JsValue::from_str("amount"), + &args.amount.to_string().into(), + )?; + + 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()?; + + 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]")))?, + }) + } }