diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bea647e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[env] +AR = "/opt/homebrew/opt/llvm/bin/llvm-ar" +CC = "/opt/homebrew/opt/llvm/bin/clang" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 338da3c..3d1af1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1266,6 +1276,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "goblin" version = "0.8.2" @@ -2001,7 +2023,6 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.22.0" -source = "git+https://github.com/payjoin/rust-payjoin.git?rev=bb47c8469146f1a9055b7f850d86f58f2b9627c6#bb47c8469146f1a9055b7f850d86f58f2b9627c6" dependencies = [ "bhttp", "bitcoin 0.32.5", @@ -2015,6 +2036,7 @@ dependencies = [ "serde", "serde_json", "url", + "web-time", ] [[package]] @@ -2049,6 +2071,9 @@ dependencies = [ "bitcoin-ohttp", "bitcoincore-rpc", "bitcoind", + "console_error_panic_hook", + "getrandom", + "gloo-timers", "hex", "http", "ohttp-relay", @@ -2056,7 +2081,9 @@ dependencies = [ "payjoin-directory", "rcgen", "reqwest", + "ring 0.17.8", "rustls 0.22.4", + "serde-wasm-bindgen", "serde_json", "testcontainers", "testcontainers-modules", @@ -2064,6 +2091,8 @@ dependencies = [ "tokio", "uniffi", "url", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2692,6 +2721,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515286919c7a28f96c362f42cd42aec7ef6d417334118be69d1ad96160aa5f5" +dependencies = [ + "fnv", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.215" @@ -3659,6 +3700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 8a62b21..d195791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,17 @@ exclude = ["tests"] [features] _danger-local-https = ["payjoin/_danger-local-https"] uniffi = ["uniffi/cli", "bitcoin-ffi/default"] +wasm = [ + "wasm-bindgen", + "console_error_panic_hook", + "getrandom", + "ring", + "gloo-timers", + "web-sys", + "serde-wasm-bindgen", +] +default = ["io"] +io = ["payjoin/io"] [lib] name = "payjoin_ffi" @@ -25,11 +36,19 @@ base64 = "0.22.1" bitcoin-ffi = { git = "https://github.com/bitcoindevkit/bitcoin-ffi.git", rev = "4cd8e644dbf4e001d71d5fffb232480fa5ff2246" } hex = "0.4.3" ohttp = { package = "bitcoin-ohttp", version = "0.6.0" } -payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", rev = "bb47c8469146f1a9055b7f850d86f58f2b9627c6", features = ["v1", "io"] } +payjoin = { path = "../../rust-payjoin/payjoin", features = ["v1"] } serde_json = "1.0.128" thiserror = "1.0.58" uniffi = { version = "0.28.0", optional = true } url = "2.5.0" +wasm-bindgen = { version = "0.2.91", optional = true } +console_error_panic_hook = { version = "0.1.7", optional = true } +# Compatibility to compile to WASM +getrandom = { version = "0.2.15", optional = true, features = ["js"] } +ring = { version = "0.17.8", optional = true, features = ["wasm32_unknown_unknown_js"] } +gloo-timers = { version = "0.3.0", optional = true, features = ["futures"] } +web-sys = { version = "0.3", optional = true, features = ["console"] } +serde-wasm-bindgen = { version = "0.2.0", optional = true } [dev-dependencies] bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39", "rpc"] } diff --git a/README.md b/README.md index 077874d..953e5cb 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,9 @@ cargo test --package payjoin_ffi --test bdk_integration_test v2_to_v2_full_cycl This project is in active development and currently in its Alpha stage. **Please proceed with caution**, particularly when using real funds. We encourage thorough review, testing, and contributions to help improve its stability and security before considering production use. + +## WASM + +```shell +wasm-pack build --no-default-features --features wasm +``` diff --git a/src/error.rs b/src/error.rs index 264fea6..c6795fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -152,6 +152,7 @@ impl From for PayjoinError { } } +#[cfg(not(feature = "wasm"))] impl From for PayjoinError { fn from(value: payjoin::io::Error) -> Self { PayjoinError::IoError { message: value.to_string() } diff --git a/src/lib.rs b/src/lib.rs index ea81a00..c7c1654 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,18 @@ pub mod bitcoin_ffi; pub mod error; +#[cfg(not(feature = "wasm"))] pub mod io; pub mod ohttp; pub mod receive; pub mod request; +#[cfg(feature = "wasm")] +pub mod request_wasm; pub mod send; pub mod uri; +mod utils; +pub use utils::*; pub use crate::bitcoin_ffi::*; pub use crate::error::PayjoinError; pub use crate::ohttp::*; @@ -20,3 +25,12 @@ pub use crate::send::uni::*; pub use crate::uri::{PjUri, Uri, Url}; #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; +// Initialize WASM logging (optional but helpful for debugging) +#[cfg(feature = "wasm")] +#[wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); +} \ No newline at end of file diff --git a/src/ohttp.rs b/src/ohttp.rs index 9f4d68a..dd51bdf 100644 --- a/src/ohttp.rs +++ b/src/ohttp.rs @@ -1,5 +1,14 @@ +#[cfg(not(feature = "wasm"))] use crate::error::PayjoinError; +#[cfg(feature = "wasm")] +use { + crate::utils::result::JsResult, + wasm_bindgen::prelude::*, +}; + +use std::str::FromStr; + impl From for OhttpKeys { fn from(value: payjoin::OhttpKeys) -> Self { Self(value) @@ -11,21 +20,44 @@ impl From for payjoin::OhttpKeys { } } #[cfg_attr(feature = "uniffi", derive(uniffi::Object))] +#[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Debug, Clone)] -pub struct OhttpKeys(pub payjoin::OhttpKeys); +pub struct OhttpKeys( + #[wasm_bindgen(skip)] + pub payjoin::OhttpKeys +); #[cfg_attr(feature = "uniffi", uniffi::export)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl OhttpKeys { /// Decode an OHTTP KeyConfig + #[cfg(not(feature = "wasm"))] #[cfg_attr(feature = "uniffi", uniffi::constructor)] pub fn decode(bytes: Vec) -> Result { payjoin::OhttpKeys::decode(bytes.as_slice()).map(|e| e.into()).map_err(|e| e.into()) } + + #[cfg(feature = "wasm")] + #[wasm_bindgen(constructor)] + pub fn decode(bytes: Vec) -> JsResult { + payjoin::OhttpKeys::decode(bytes.as_slice()) + .map(|e| e.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } + + #[cfg(feature = "wasm")] + pub fn parse(s: &str) -> JsResult { + payjoin::OhttpKeys::from_str(s) + .map(|e| e.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } } + use std::sync::Mutex; #[cfg_attr(feature = "uniffi", derive(uniffi::Object))] +#[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct ClientResponse(Mutex>); impl From<&ClientResponse> for ohttp::ClientResponse { diff --git a/src/receive/mod.rs b/src/receive/mod.rs index 657f3df..7706ac2 100644 --- a/src/receive/mod.rs +++ b/src/receive/mod.rs @@ -12,8 +12,13 @@ use crate::{ClientResponse, Request}; #[cfg(feature = "uniffi")] pub mod uni; +#[cfg(feature = "wasm")] +pub mod wasm; + + #[derive(Clone, Debug)] pub struct Receiver(pub payjoin::receive::v2::Receiver); + impl From for payjoin::receive::v2::Receiver { fn from(value: Receiver) -> Self { value.0 @@ -246,7 +251,7 @@ impl WantsOutputs { replacement_outputs.iter().map(|o| o.clone().into()).collect(); self.0 .clone() - .replace_receiver_outputs(replacement_outputs, &drain_script.0) + .replace_receiver_outputs(replacement_outputs.into_iter(), &drain_script.0) .map(Into::into) .map_err(Into::into) } diff --git a/src/receive/wasm.rs b/src/receive/wasm.rs new file mode 100644 index 0000000..c7cea9f --- /dev/null +++ b/src/receive/wasm.rs @@ -0,0 +1,430 @@ +use std::str::FromStr; +use std::time::Duration; +use std::sync::Arc; + +// use payjoin::bitcoin::psbt::Psbt; +// use payjoin::bitcoin::FeeRate; +// use payjoin::receive as pdk; + +use crate::bitcoin_ffi::Network; +// use crate::error::PayjoinError; + +use crate::ohttp::{ClientResponse, OhttpKeys}; +use crate::request_wasm::Request; +use crate::Url; +use crate::error::PayjoinError; + +use { + crate::utils::result::JsResult, + wasm_bindgen::prelude::*, + wasm_bindgen::JsValue, + web_sys::console, + web_sys::js_sys, + // web_sys::js_sys::Date, + serde_wasm_bindgen, +}; + + +#[wasm_bindgen] +#[derive(Clone, Debug)] +pub struct Receiver(super::Receiver); + +impl From for Receiver { + fn from(receiver: payjoin::receive::v2::Receiver) -> Self { + Self(receiver.into()) + } +} + +#[wasm_bindgen] +impl Receiver { + pub fn new( + address: String, + network: String, + directory: String, + ohttp_keys: OhttpKeys, + expire_after: Option, + ) -> JsResult { + + // Parse network string + let network = Network::from_str(&network) + .map_err(|_| wasm_bindgen::JsError::new("Invalid network"))?; + + // Parse URLs + // let directory = Url::parse(directory) + // .map_err(|_| wasm_bindgen::JsError::new("Invalid directory URL"))?; + // let ohttp_relay = Url::parse(ohttp_relay) + // .map_err(|_| wasm_bindgen::JsError::new("Invalid relay URL"))?; + + // Parse OHTTP keys from JSON string + // let ohttp_keys: OhttpKeys = OhttpKeys::parse(&ohttp_keys) + // .map_err(|_| wasm_bindgen::JsError::new("Invalid OHTTP keys"))?; + + // Parse Bitcoin address and verify network + let address = payjoin::bitcoin::Address::from_str(&address) + .map_err(|_| wasm_bindgen::JsError::new("Invalid Bitcoin address"))? + .require_network(network) + .map_err(|_| wasm_bindgen::JsError::new("Address network mismatch"))?; + + Ok(payjoin::receive::v2::Receiver::new( + address, + directory, + ohttp_keys.into(), + expire_after.map(Duration::from_secs) + ) + .map_err(PayjoinError::from)? + .into()) + } + + pub fn pj_uri(&self) -> crate::PjUri { + self.0.pj_uri().into() + } + + pub fn extract_req(&self, ohttp_relay: String) -> JsResult { + self.0 + .extract_req(ohttp_relay) + .map(|(request, ctx)| RequestResponse::new(request.into(), ctx)) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } + + ///The response can either be an UncheckedProposal or an ACCEPTED message indicating no UncheckedProposal is available yet. + pub fn process_res( + &self, + body: &[u8], + context: ClientResponse, + ) -> JsResult> { + self.0 + .process_res(body, &context) + .map(|e| e.map(|x| x.into())) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } + + // /// The contents of the `&pj=` query parameter including the base64url-encoded public key receiver subdirectory. + // /// This identifies a session at the payjoin directory server. + // #[cfg(feature = "uniffi")] + // pub fn pj_url(&self) -> Arc { + // Arc::new(self.0.pj_url()) + // } + ///The per-session public key to use as an identifier + pub fn id(&self) -> String { + self.0.id() + } + + pub fn to_json(&self) -> JsResult { + self.0.to_json() + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } + + // pub fn from_json(json: &str) -> JsResult { + // super::Receiver::from_json(json) + // .map(Into::into) + // .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + // } +} + +#[wasm_bindgen] +pub struct RequestResponse(Request, ClientResponse); + +#[wasm_bindgen] +impl RequestResponse { + #[wasm_bindgen(constructor)] + pub fn new(request: Request, client_response: ClientResponse) -> Self { + Self(request, client_response) + } + + #[wasm_bindgen(getter)] + pub fn request(&self) -> Request { + self.0.clone() + } + + // consumes self, so RequestResponse won't be available in js after getting client_response + #[wasm_bindgen(getter)] + pub fn client_response(self) -> ClientResponse { + self.1 + } +} + +#[derive(Clone)] +#[wasm_bindgen] +pub struct UncheckedProposal(super::UncheckedProposal); + +impl From for UncheckedProposal { + fn from(value: super::UncheckedProposal) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl UncheckedProposal { + pub fn check_broadcast_suitability( + &self, + min_fee_rate: Option, + can_broadcast: Option,//Box,//fn to check tx can broadcast + ) -> JsResult { + self.0 + .clone() + .check_broadcast_suitability(min_fee_rate, |transaction| { + // should actually check if the transaction can be broadcast + Ok(can_broadcast.unwrap_or(false)) + }) + .map(|e| e.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } +} + +#[derive(Clone)] +#[wasm_bindgen] +pub struct MaybeInputsOwned(super::MaybeInputsOwned); + +impl From for MaybeInputsOwned { + fn from(value: super::MaybeInputsOwned) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl MaybeInputsOwned { + ///Check that the Original PSBT has no receiver-owned inputs. Return original-psbt-rejected error or otherwise refuse to sign undesirable inputs. + /// An attacker could try to spend receiver's own inputs. This check prevents that. + pub fn check_inputs_not_owned( + &self, + is_owned: js_sys::Function, + ) -> JsResult { + self.0 + .check_inputs_not_owned(|input| { + let result = is_owned.call1(&JsValue::NULL, &js_sys::Uint8Array::from(&input[..])) + .map_err(|e| PayjoinError::UnexpectedError { + message: e.as_string().unwrap_or_else(|| "Unknown JS error".to_string()) + })?; + result.as_bool() + .ok_or_else(|| PayjoinError::UnexpectedError { + message: "Function must return boolean".to_string() + }) + }) + .map(|t| t.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } +} + +#[derive(Clone)] +#[wasm_bindgen] +pub struct MaybeInputsSeen(super::MaybeInputsSeen); + +impl From for MaybeInputsSeen { + fn from(value: super::MaybeInputsSeen) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl MaybeInputsSeen { + /// Make sure that the original transaction inputs have never been seen before. This prevents probing attacks. This prevents reentrant Payjoin, where a sender proposes a Payjoin PSBT as a new Original PSBT for a new Payjoin. + pub fn check_no_inputs_seen_before( + &self, + is_known: js_sys::Function, + ) -> JsResult { + self.0 + .clone() + .check_no_inputs_seen_before(|outpoint| { + // Convert the outpoint to a JsValue and call the callback + is_known.call1(&JsValue::null(), &serde_wasm_bindgen::to_value(outpoint).map_err(|e| PayjoinError::UnexpectedError { + message: e.to_string(), + })?) + .map(|result| result.as_bool().unwrap_or(false)) + .map(Ok) + .unwrap_or(Ok(false)) + }) + .map(|t| t.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } +} + +#[derive(Clone)] +#[wasm_bindgen] +pub struct OutputsUnknown(super::OutputsUnknown); + +impl From for OutputsUnknown { + fn from(value: super::OutputsUnknown) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl OutputsUnknown { + /// Find which outputs belong to the receiver + pub fn identify_receiver_outputs( + &self, + is_receiver_output: js_sys::Function, + ) -> JsResult { + self.0 + .clone() + .identify_receiver_outputs(|output_script| { + is_receiver_output.call1(&JsValue::null(), &js_sys::Uint8Array::from(&output_script[..])) + .map(|result| result.as_bool().unwrap_or(false)) + .map_err(|e| PayjoinError::UnexpectedError { + message: e.as_string().unwrap_or_else(|| "Unknown JS error".to_string()) + }) + }) + .map(|t| t.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } +} + +#[wasm_bindgen] +pub struct WantsOutputs(super::WantsOutputs); + +impl From for WantsOutputs { + fn from(value: super::WantsOutputs) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl WantsOutputs { + pub fn commit_outputs(&self) -> WantsInputs { + self.0.commit_outputs().into() + } +} + +#[wasm_bindgen] +pub struct WantsInputs(super::WantsInputs); + +impl From for WantsInputs { + fn from(value: super::WantsInputs) -> Self { + Self(value) + } +} +#[wasm_bindgen] +impl WantsInputs { + + pub fn contribute_inputs( + &self, + replacement_inputs: Vec, + ) -> JsResult { + let replacement_inputs: Vec = replacement_inputs + .into_iter() + .map(|pair| pair.0) + .collect(); + self.0.contribute_inputs(replacement_inputs) + .map(|t| t.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } + + pub fn commit_inputs(&self) -> ProvisionalProposal { + self.0.commit_inputs().into() + } +} + +#[wasm_bindgen] +pub struct InputPair(super::InputPair); + +impl From for InputPair { + fn from(value: super::InputPair) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl InputPair { + pub fn new( + txid: String, + vout: u32, + value: u64, + script_pubkey: Vec, + ) -> Self { + let txin = bitcoin_ffi::TxIn { + previous_output: bitcoin_ffi::OutPoint { + txid: bitcoin_ffi::Txid::from_str(&txid).unwrap(), + vout, + }, + script_sig: Arc::new(bitcoin_ffi::Script::new(Vec::new())), + sequence: 0xffffffff, + witness: Vec::new(), + }; + // console::log_1(&JsValue::from_str(&format!("InputPair::new: txid={}, vout={}, value={}, script_pubkey={}", txid, vout, value, script_pubkey))); + let psbtin = crate::bitcoin_ffi::PsbtInput { + witness_utxo: Some(bitcoin_ffi::TxOut { + value: Arc::new(bitcoin_ffi::Amount::from_sat(value)), + script_pubkey: Arc::new(bitcoin_ffi::Script::new(script_pubkey)), + }), + redeem_script: None, + witness_script: None, + }; + Self(super::InputPair::new(txin, psbtin).unwrap()) + } +} + +#[wasm_bindgen] +pub struct ProvisionalProposal(super::ProvisionalProposal); + +impl From for ProvisionalProposal { + fn from(value: super::ProvisionalProposal) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl ProvisionalProposal { + pub fn finalize_proposal( + &self, + process_psbt: js_sys::Function, + min_feerate_sat_per_vb: Option, + max_effective_fee_rate_sat_per_vb: Option, + ) -> JsResult { + self.0 + .finalize_proposal( + |psbt| { + process_psbt.call1(&JsValue::null(), &JsValue::from_str(&psbt.to_string())) + .map_err(|e| PayjoinError::UnexpectedError { + message: e.as_string().unwrap_or_else(|| "Unknown JS error".to_string()) + })? + .as_string() + .ok_or_else(|| PayjoinError::UnexpectedError { + message: "Process PSBT must return string".to_string() + }) + }, + min_feerate_sat_per_vb, + max_effective_fee_rate_sat_per_vb, + ) + .map(|e| e.into()) + .map_err(|e| wasm_bindgen::JsError::new(&e.to_string())) + } +} + +#[derive(Clone)] +#[wasm_bindgen] +pub struct PayjoinProposal(super::PayjoinProposal); + +impl From for super::PayjoinProposal { + fn from(value: PayjoinProposal) -> Self { + value.0 + } +} + +impl From for PayjoinProposal { + fn from(value: super::PayjoinProposal) -> Self { + Self(value) + } +} + +#[wasm_bindgen] +impl PayjoinProposal { + + pub fn extract_v2_req( + &self, + ohttp_relay: String, + ) -> JsResult { + match self.0.clone().extract_v2_req(ohttp_relay) { + Ok((req, ctx)) => Ok(RequestResponse::new(req.into(), ctx.into())), + Err(e) => Err(wasm_bindgen::JsError::new(&e.to_string())) + } + } + + pub fn process_res( + &self, + body: &[u8], + ohttp_context: &ClientResponse, + ) -> JsResult<()> { + self.0.process_res(body, ohttp_context.into()) + .map_err(|e| e.into()) + } +} \ No newline at end of file diff --git a/src/request_wasm.rs b/src/request_wasm.rs new file mode 100644 index 0000000..619f1ce --- /dev/null +++ b/src/request_wasm.rs @@ -0,0 +1,50 @@ +use wasm_bindgen::prelude::*; +use crate::request::Request as PdkRequest; + +/// Represents data that needs to be transmitted to the receiver. +/// You need to send this request over HTTP(S) to the receiver. +#[wasm_bindgen] +#[derive(Clone, Debug)] +pub struct Request { + url: String, + content_type: String, + body: Vec +} + +#[wasm_bindgen] +impl Request { + #[wasm_bindgen(getter)] + pub fn url(&self) -> String { + self.url.clone() + } + + #[wasm_bindgen(getter)] + pub fn content_type(&self) -> String { + self.content_type.clone() + } + + #[wasm_bindgen(getter)] + pub fn body(&self) -> Vec { + self.body.clone() + } +} + +impl From for Request { + fn from(value: payjoin::Request) -> Self { + Self { + url: value.url.to_string(), + content_type: value.content_type.to_string(), + body: value.body, + } + } +} + +impl From for Request { + fn from(value: PdkRequest) -> Self { + Self { + url: value.url.as_ref().as_string(), + content_type: value.content_type.to_string(), + body: value.body, + } + } +} \ No newline at end of file diff --git a/src/send/mod.rs b/src/send/mod.rs index cd564d3..c2c8456 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -10,6 +10,9 @@ use crate::uri::{PjUri, Url}; #[cfg(feature = "uniffi")] pub mod uni; +#[cfg(feature = "wasm")] +pub mod wasm; + ///Builder for sender-side payjoin parameters /// ///These parameters define how client wants to handle Payjoin. diff --git a/src/send/wasm.rs b/src/send/wasm.rs new file mode 100644 index 0000000..e25bfe0 --- /dev/null +++ b/src/send/wasm.rs @@ -0,0 +1,160 @@ +use crate::ohttp::ClientResponse; + +use crate::Url; + +use { + crate::utils::result::JsResult, + wasm_bindgen::prelude::*, + wasm_bindgen::JsValue, + web_sys::console, + // web_sys::js_sys::Date, +}; +use crate::uri::PjUri; +use crate::request_wasm::Request; + +#[wasm_bindgen] +#[derive(Clone)] +pub struct SenderBuilder(super::SenderBuilder); + +impl From> for SenderBuilder { + fn from(sender: payjoin::send::v2::SenderBuilder<'static>) -> Self { + Self(super::SenderBuilder::from(sender)) + } +} + +impl From for SenderBuilder { + fn from(sender: super::SenderBuilder) -> Self { + Self(sender) + } +} + +#[wasm_bindgen] +impl SenderBuilder { + pub fn from_psbt_and_uri(psbt: String, uri: PjUri) -> JsResult { + console::log_1(&JsValue::from_str(&format!("SenderBuilder::from_psbt_and_uri: psbt={}, uri={}", psbt, uri.as_string()))); + super::SenderBuilder::from_psbt_and_uri(psbt, uri) + .map(Into::into) + .map_err(Into::into) + } + + pub fn build_recommended(&self, min_fee_rate: u64) -> JsResult { + self.0 + .clone() + .build_recommended(min_fee_rate) + .map(Into::into) + .map_err(Into::into) + } +} + + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Sender(payjoin::send::v2::Sender); + +impl From for Sender { + fn from(value: payjoin::send::v2::Sender) -> Self { + Self(value) + } +} + +impl From for Sender { + fn from(sender: super::Sender) -> Self { + Self(sender.0) + } +} +#[wasm_bindgen] +impl Sender { + /// Extract serialized Request and Context from a Payjoin Proposal. + pub fn extract_v2(&self, ohttp_relay: String) -> JsResult { + let url = Url::parse(ohttp_relay)?; + match self.0.extract_v2(url.into()) { + Ok((req, ctx)) => Ok(RequestV2PostContext::new(req.into(), ctx.into())), + Err(e) => Err(e.into()), + } + } +} + +#[wasm_bindgen] +pub struct V2PostContext(payjoin::send::v2::V2PostContext); + +impl From for V2PostContext { + fn from(ctx: payjoin::send::v2::V2PostContext) -> Self { + Self(ctx) + } +} + +#[wasm_bindgen] +impl V2PostContext { + // consumes self, so V2PostContext won't be available in js after calling this + pub fn process_response(self, response: &[u8]) -> JsResult { + self.0.process_response(response) + .map(Into::into) + .map_err(Into::into) + } +} + +#[wasm_bindgen] +pub struct V2GetContext(payjoin::send::v2::V2GetContext); + +impl From for V2GetContext { + fn from(ctx: payjoin::send::v2::V2GetContext) -> Self { + Self(ctx) + } +} + +#[wasm_bindgen] +impl V2GetContext { + pub fn extract_req(&self, ohttp_relay: String) -> JsResult { + self.0.extract_req(payjoin::Url::parse(&ohttp_relay)?) + .map(|(request, ctx)| RequestOhttpContext::new(request.into(), ctx.into())) + .map_err(Into::into) + } + + pub fn process_response(&self, response: &[u8], ohttp_ctx: &ClientResponse) -> JsResult> { + self.0.process_response(response, ohttp_ctx.into()) + .map(|opt_psbt| opt_psbt.map(|psbt| psbt.to_string())) + .map_err(Into::into) + } +} + +#[wasm_bindgen] +pub struct RequestOhttpContext (Request, ClientResponse); + +#[wasm_bindgen] +impl RequestOhttpContext { + pub fn new(request: Request, ohttp_ctx: ClientResponse) -> Self { + Self(request, ohttp_ctx) + } + + #[wasm_bindgen(getter)] + pub fn request(&self) -> Request { + self.0.clone() + } + + #[wasm_bindgen(getter)] + pub fn ohttp_ctx(self) -> ClientResponse { + self.1 + } +} + +#[wasm_bindgen] +pub struct RequestV2PostContext (Request, V2PostContext); + +#[wasm_bindgen] +impl RequestV2PostContext { + #[wasm_bindgen(constructor)] + pub fn new(request: Request, context: V2PostContext) -> Self { + Self(request, context) + } + + #[wasm_bindgen(getter)] + pub fn request(&self) -> Request { + self.0.clone() // Assuming Request implements Clone + } + + // consumes self, so RequestV2PostContext won't be available in js after getting context, however, using destructuring on the js end makes this seemless. + #[wasm_bindgen(getter)] + pub fn context(self) -> V2PostContext { + self.1 + } +} diff --git a/src/uri.rs b/src/uri.rs index 2f95a69..a7bdd1c 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -1,12 +1,18 @@ use std::str::FromStr; #[cfg(feature = "uniffi")] use std::sync::Arc; +#[cfg(feature = "wasm")] +use { + crate::utils::result::JsResult, + wasm_bindgen::prelude::*, +}; use payjoin::bitcoin::address::NetworkChecked; use payjoin::UriExt; use crate::error::PayjoinError; #[derive(Clone)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct Uri(payjoin::Uri<'static, NetworkChecked>); impl From for payjoin::Uri<'static, NetworkChecked> { fn from(value: Uri) -> Self { @@ -20,13 +26,24 @@ impl From> for Uri { } } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl Uri { + #[cfg(not(feature = "wasm"))] pub fn parse(uri: String) -> Result { match payjoin::Uri::from_str(uri.as_str()) { Ok(e) => Ok(e.assume_checked().into()), Err(e) => Err(PayjoinError::PjParseError { message: e.to_string() }), } } + + #[cfg(feature = "wasm")] + pub fn parse(uri: String) -> JsResult { + match payjoin::Uri::from_str(uri.as_str()) { + Ok(e) => Ok(e.assume_checked().into()), + Err(e) => Err(wasm_bindgen::JsError::new(&e.to_string())), + } + } + pub fn address(&self) -> String { self.clone().0.address.to_string() } @@ -40,7 +57,7 @@ impl Uri { pub fn message(&self) -> Option { self.0.message.clone().and_then(|x| String::try_from(x).ok()) } - #[cfg(not(feature = "uniffi"))] + #[cfg(not(any(feature = "uniffi", feature = "wasm")))] pub fn check_pj_supported(&self) -> Result { match self.0.clone().check_pj_supported() { Ok(e) => Ok(e.into()), @@ -62,6 +79,15 @@ impl Uri { } } } + #[cfg(feature = "wasm")] + pub fn check_pj_supported(&self) -> JsResult { + match self.0.clone().check_pj_supported() { + Ok(e) => Ok(e.into()), + Err(_) => { + Err(wasm_bindgen::JsError::new("Uri doesn't support payjoin")) + } + } + } pub fn as_string(&self) -> String { self.0.clone().to_string() } @@ -81,22 +107,31 @@ impl<'a> From for payjoin::PjUri<'a> { #[derive(Clone)] #[cfg_attr(feature = "uniffi", derive(uniffi::Object))] -pub struct PjUri(pub payjoin::PjUri<'static>); +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub struct PjUri( + #[wasm_bindgen(skip)] + pub payjoin::PjUri<'static> +); -#[cfg_attr(feature = "uniffi", uniffi::export)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl PjUri { + #[wasm_bindgen(getter)] pub fn address(&self) -> String { self.0.clone().address.to_string() } + + #[wasm_bindgen(getter)] /// Number of sats requested as payment pub fn amount_sats(&self) -> Option { self.0.clone().amount.map(|e| e.to_sat()) } + #[wasm_bindgen(getter)] pub fn pj_endpoint(&self) -> String { self.0.extras.endpoint().to_string() } + #[wasm_bindgen(getter)] pub fn as_string(&self) -> String { self.0.clone().to_string() } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..d11ca02 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod result; \ No newline at end of file diff --git a/src/utils/result.rs b/src/utils/result.rs new file mode 100644 index 0000000..40f538b --- /dev/null +++ b/src/utils/result.rs @@ -0,0 +1,3 @@ +use wasm_bindgen::JsError; + +pub type JsResult = Result;