From f5a587d8c891ec28849e46a4b120daf75f29a13c Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 1/6] Add .idea/ to gitignore I have been using RustRover for development, and it generates and saves configurations in .idea/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1c5d75631..50d8501d6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ mutants.out* *.ikm *payjoin.sqlite data +.idea From a8c7df820a2a8266937945220ebb85f4fedd7a11 Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 2/6] Revert io module removal from payjoin-ffi Also Allow IoError inner message to be read via a uniffi export --- payjoin-ffi/Cargo.toml | 1 + payjoin-ffi/javascript/contrib/test.sh | 0 payjoin-ffi/src/io.rs | 62 ++++++++++++++++++++++++++ payjoin-ffi/src/lib.rs | 2 + 4 files changed, 65 insertions(+) mode change 100644 => 100755 payjoin-ffi/javascript/contrib/test.sh create mode 100644 payjoin-ffi/src/io.rs diff --git a/payjoin-ffi/Cargo.toml b/payjoin-ffi/Cargo.toml index 5c30007fc..7e1dd8306 100644 --- a/payjoin-ffi/Cargo.toml +++ b/payjoin-ffi/Cargo.toml @@ -9,6 +9,7 @@ exclude = ["tests"] default = [] csharp = ["dep:uniffi-bindgen-cs"] dart = ["dep:uniffi-dart"] +io = ["payjoin/io"] _test-utils = ["payjoin-test-utils", "tokio"] _manual-tls = ["payjoin/_manual-tls"] wasm_js = ["getrandom/js"] diff --git a/payjoin-ffi/javascript/contrib/test.sh b/payjoin-ffi/javascript/contrib/test.sh old mode 100644 new mode 100755 diff --git a/payjoin-ffi/src/io.rs b/payjoin-ffi/src/io.rs new file mode 100644 index 000000000..b22f79345 --- /dev/null +++ b/payjoin-ffi/src/io.rs @@ -0,0 +1,62 @@ +pub use error::IoError; + +use crate::ohttp::OhttpKeys; + +pub mod error { + #[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Object)] + #[error("IO error: {message}")] + pub struct IoError { + message: String, + } + + #[uniffi::export] + impl IoError { + pub fn message(&self) -> String { self.message.clone() } + } + + impl From for IoError { + fn from(value: payjoin::io::Error) -> Self { IoError { message: format!("{value:?}") } } + } +} + +/// Fetch the ohttp keys from the specified payjoin directory via proxy. +/// +/// * `ohttp_relay`: The http CONNECT method proxy to request the ohttp keys from a payjoin +/// directory. Proxying requests for ohttp keys ensures a client IP address is never revealed to +/// the payjoin directory. +/// +/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This +/// directory stores and forwards payjoin client payloads. +#[uniffi::export] +pub async fn fetch_ohttp_keys( + ohttp_relay: &str, + payjoin_directory: &str, +) -> Result { + payjoin::io::fetch_ohttp_keys(ohttp_relay, payjoin_directory) + .await + .map(|e| e.into()) + .map_err(|e| e.into()) +} + +/// Fetch the ohttp keys from the specified payjoin directory via proxy. +/// +/// * `ohttp_relay`: The http CONNECT method proxy to request the ohttp keys from a payjoin +/// directory. Proxying requests for ohttp keys ensures a client IP address is never revealed to +/// the payjoin directory. +/// +/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This +/// directory stores and forwards payjoin client payloads. +/// +/// * `cert_der`: The DER-encoded certificate to use for local HTTPS connections. +#[cfg(feature = "_manual-tls")] +#[uniffi::export] +pub async fn fetch_ohttp_keys_with_cert( + ohttp_relay: &str, + payjoin_directory: &str, + cert_der: Vec, +) -> Result { + payjoin::io::fetch_ohttp_keys_with_cert(ohttp_relay, payjoin_directory, &cert_der) + .await + .map(|e| e.into()) + .map_err(|e| e.into()) +} diff --git a/payjoin-ffi/src/lib.rs b/payjoin-ffi/src/lib.rs index a0e600a1f..dff2aa971 100644 --- a/payjoin-ffi/src/lib.rs +++ b/payjoin-ffi/src/lib.rs @@ -1,6 +1,8 @@ #![crate_name = "payjoin_ffi"] pub mod error; +#[cfg(feature = "io")] +pub mod io; pub mod ohttp; pub mod output_substitution; pub mod receive; From c33e8caf1926bc47fc4eb5d17de83a05fa00f879 Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 3/6] Add polyfills for node < 21 support Adds pollyfills for WebSocket and File API (required by unidici). Node > 21 actually doesn't need these pollyfills, neither does the browser. I've not tested these changes in the browser yet. --- payjoin-ffi/javascript/package-lock.json | 63 ++++++++++++++++++++++++ payjoin-ffi/javascript/package.json | 6 +++ payjoin-ffi/javascript/src/index.ts | 2 + payjoin-ffi/javascript/src/polyfills.ts | 23 +++++++++ 4 files changed, 94 insertions(+) create mode 100644 payjoin-ffi/javascript/src/polyfills.ts diff --git a/payjoin-ffi/javascript/package-lock.json b/payjoin-ffi/javascript/package-lock.json index a706b7c18..6e6e4d62d 100644 --- a/payjoin-ffi/javascript/package-lock.json +++ b/payjoin-ffi/javascript/package-lock.json @@ -7,7 +7,13 @@ "": { "name": "payjoin", "version": "0.1.0", + "dependencies": { + "undici": "^7.24.3", + "ws": "^8.19.0" + }, "devDependencies": { + "@types/node": "^25.5.0", + "@types/ws": "^8.18.1", "prettier": "^3.6.2", "tsx": "^4.20.6", "typescript": "^5.9.3", @@ -456,6 +462,26 @@ "node": ">=18" } }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -586,6 +612,22 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/uniffi-bindgen-react-native": { "version": "0.30.0-1", "resolved": "https://registry.npmjs.org/uniffi-bindgen-react-native/-/uniffi-bindgen-react-native-0.30.0-1.tgz", @@ -596,6 +638,27 @@ "ubrn": "bin/cli.cjs", "uniffi-bindgen-react-native": "bin/cli.cjs" } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/payjoin-ffi/javascript/package.json b/payjoin-ffi/javascript/package.json index 85600b612..4685c9cad 100644 --- a/payjoin-ffi/javascript/package.json +++ b/payjoin-ffi/javascript/package.json @@ -26,6 +26,8 @@ "rust" ], "devDependencies": { + "@types/node": "^25.5.0", + "@types/ws": "^8.18.1", "prettier": "^3.6.2", "tsx": "^4.20.6", "typescript": "^5.9.3", @@ -35,5 +37,9 @@ "type": "git", "url": "https://github.com/payjoin/rust-payjoin.git", "directory": "payjoin-ffi/javascript" + }, + "dependencies": { + "undici": "^7.24.3", + "ws": "^8.19.0" } } diff --git a/payjoin-ffi/javascript/src/index.ts b/payjoin-ffi/javascript/src/index.ts index 09b82dfb4..fffdf0138 100644 --- a/payjoin-ffi/javascript/src/index.ts +++ b/payjoin-ffi/javascript/src/index.ts @@ -1,3 +1,5 @@ +import "./polyfills.js"; + // Export the generated bindings to the app. export * as payjoin from "./generated/payjoin.js"; diff --git a/payjoin-ffi/javascript/src/polyfills.ts b/payjoin-ffi/javascript/src/polyfills.ts new file mode 100644 index 000000000..eaeb25fce --- /dev/null +++ b/payjoin-ffi/javascript/src/polyfills.ts @@ -0,0 +1,23 @@ +import buffer from "node:buffer"; +import ws from "ws"; + + +// Reqired for Node.JS version < 20, +// buffer.File is not available in all v18 and v19 versions as it was backported +if (typeof globalThis.File === "undefined") { + if (buffer.File) { + (globalThis.File as any) = buffer.File; + } else { + console.warn("File is missing and not found in buffer module"); + } +} + +// WebSocket polyfill is needed for Node.JS version < 21 +// This will work for v18+ +if (typeof globalThis.WebSocket === "undefined") { + try { + (globalThis.WebSocket as any) = ws; + } catch (e) { + console.error("Failed to load WebSocket polyfill", e); + } +} From ed3e0c3daf8eda6bc90987f9f5c089c3489c228f Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 4/6] Implement WASM comptaible implementation of io. Pure Rust implementation of fetch_ohttp_keys using gloo-net and (too much rust-wasmbindgen). The Websocket Implementation is quite straightfoward as it uses gloo-net to access the WebSocket API. However, the fetch API does not support Proxy requests, so we bind to uncidi and (in a quite ugly fashion) make the HTTP(S) request thru the proxy. Messy! It would likely have been more sensible to implement these as JS stubs, but I had quite a lot of fun implementing these. lockfiles were generated by using the script in contrib/ +----------------------+ | fetch_ohttp_keys() | +----------+-----------+ | v +----------+-----------+ | Detect JS Runtime | +----+------------+----+ | | (process) (Browser/Worker) | | v v +---------+ (fail) +-------------------+ | undici |--------->| WebSocket + | | Proxy | | rustls TLS Stream | +---------+ +-------------------+ | | | (success) | v v +----------------------------------------+ | OHTTP Relay | +----------------------------------------+ | v +----------------------------------------+ | Payjoin Directory | +----------------------------------------+ Closes: https://github.com/payjoin/rust-payjoin/issues/1250 --- Cargo-minimal.lock | 342 ++++++++++++++++++--- Cargo-recent.lock | 342 ++++++++++++++++++--- payjoin/Cargo.toml | 15 +- payjoin/src/core/io/mod.rs | 66 +++++ payjoin/src/core/{io.rs => io/native.rs} | 72 +---- payjoin/src/core/io/wasm.rs | 360 +++++++++++++++++++++++ 6 files changed, 1043 insertions(+), 154 deletions(-) create mode 100644 payjoin/src/core/io/mod.rs rename payjoin/src/core/{io.rs => io/native.rs} (75%) create mode 100644 payjoin/src/core/io/wasm.rs diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index f05545562..6ba4db15f 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -802,13 +802,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.105" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ + "find-msvc-tools", "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -1466,6 +1467,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.9" @@ -1585,6 +1592,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.31", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1678,11 +1696,26 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.4.4" @@ -1699,6 +1732,41 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.63", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "goblin" version = "0.8.2" @@ -1951,7 +2019,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -2087,6 +2155,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2209,9 +2283,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2257,6 +2331,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.174" @@ -2729,22 +2809,35 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" name = "payjoin" version = "0.25.0" dependencies = [ + "base64 0.21.7", "bhttp", "bitcoin 0.32.8", "bitcoin-hpke", "bitcoin-ohttp", "bitcoin_uri", + "futures", + "futures-rustls", + "getrandom 0.4.2", + "gloo-net", + "gloo-utils", "http", + "js-sys", "once_cell", "payjoin-test-utils", "reqwest", + "ring", "rustls 0.23.31", + "rustls-pki-types", "serde", + "serde-wasm-bindgen", "serde_json", "tokio", "tracing", "url", + "wasm-bindgen", + "wasm-bindgen-futures", "web-time", + "webpki-roots 1.0.6", ] [[package]] @@ -3049,6 +3142,16 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3175,6 +3278,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -3404,20 +3513,19 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -3541,9 +3649,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -3755,6 +3863,17 @@ dependencies = [ "typeid", ] +[[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" @@ -3895,6 +4014,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3990,12 +4115,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4332,7 +4451,7 @@ dependencies = [ "time", "tokio", "tokio-rustls 0.26.2", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", "x509-parser 0.18.0", ] @@ -4653,6 +4772,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uniffi" version = "0.30.0" @@ -4974,28 +5099,33 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -5013,9 +5143,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5023,26 +5153,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.106", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.10.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.6.0", + "hashbrown 0.15.5", + "indexmap 2.10.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.70" @@ -5094,14 +5258,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5394,6 +5558,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -5403,6 +5587,74 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.10.0", + "prettyplease", + "syn 2.0.106", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.106", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "indexmap 2.10.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.10.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index f05545562..6ba4db15f 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -802,13 +802,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.105" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ + "find-msvc-tools", "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -1466,6 +1467,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.9" @@ -1585,6 +1592,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.31", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1678,11 +1696,26 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.4.4" @@ -1699,6 +1732,41 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.63", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "goblin" version = "0.8.2" @@ -1951,7 +2019,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -2087,6 +2155,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2209,9 +2283,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2257,6 +2331,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.174" @@ -2729,22 +2809,35 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" name = "payjoin" version = "0.25.0" dependencies = [ + "base64 0.21.7", "bhttp", "bitcoin 0.32.8", "bitcoin-hpke", "bitcoin-ohttp", "bitcoin_uri", + "futures", + "futures-rustls", + "getrandom 0.4.2", + "gloo-net", + "gloo-utils", "http", + "js-sys", "once_cell", "payjoin-test-utils", "reqwest", + "ring", "rustls 0.23.31", + "rustls-pki-types", "serde", + "serde-wasm-bindgen", "serde_json", "tokio", "tracing", "url", + "wasm-bindgen", + "wasm-bindgen-futures", "web-time", + "webpki-roots 1.0.6", ] [[package]] @@ -3049,6 +3142,16 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3175,6 +3278,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -3404,20 +3513,19 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -3541,9 +3649,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -3755,6 +3863,17 @@ dependencies = [ "typeid", ] +[[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" @@ -3895,6 +4014,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3990,12 +4115,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4332,7 +4451,7 @@ dependencies = [ "time", "tokio", "tokio-rustls 0.26.2", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", "x509-parser 0.18.0", ] @@ -4653,6 +4772,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uniffi" version = "0.30.0" @@ -4974,28 +5099,33 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -5013,9 +5143,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5023,26 +5153,60 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.106", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.10.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.6.0", + "hashbrown 0.15.5", + "indexmap 2.10.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.70" @@ -5094,14 +5258,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5394,6 +5558,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -5403,6 +5587,74 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.10.0", + "prettyplease", + "syn 2.0.106", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.106", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "indexmap 2.10.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.10.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/payjoin/Cargo.toml b/payjoin/Cargo.toml index 705b9cde1..200912563 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -50,7 +50,7 @@ reqwest = { version = "0.12.23", default-features = false, optional = true } rustls = { version = "0.23.31", optional = true, default-features = false, features = [ "ring", ] } -serde = { version = "1.0.219", default-features = false, optional = true } +serde = { version = "1.0.219", default-features = false, optional = true, features = ["derive"] } serde_json = { version = "1.0.142", optional = true } tracing = "0.1.41" url = { version = "2.5.4", optional = true, default-features = false, features = [ @@ -59,6 +59,19 @@ url = { version = "2.5.4", optional = true, default-features = false, features = [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = "1.1.0" +getrandom = { version = "0.4.2", features = ["wasm_js"] } +js-sys = "0.3.85" +gloo-net = { version = "0.6.0", features = ["io-util"] } +gloo-utils = { version = "0.2.0", features = ["serde"] } +ring = { version = "0.17.14", default-features = false, features = ["wasm32_unknown_unknown_js"] } +futures = { version = "0.3.28"} +futures-rustls = { version = "0.26.0", default-features = false, features = ["ring"] } +webpki-roots = "1.0.5" +rustls-pki-types = { version = "1.14.0", default-features = false, features = ["web"] } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +serde-wasm-bindgen = "0.6" +base64 = "0.21" [dev-dependencies] once_cell = "1.21.3" diff --git a/payjoin/src/core/io/mod.rs b/payjoin/src/core/io/mod.rs new file mode 100644 index 000000000..63b4a8cb2 --- /dev/null +++ b/payjoin/src/core/io/mod.rs @@ -0,0 +1,66 @@ +//! IO-related types and functions. Specifically, fetching OHTTP keys from a payjoin directory. + +#[cfg(not(target_arch = "wasm32"))] +mod native; +#[cfg(not(target_arch = "wasm32"))] +use native::InternalErrorInner; +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; + +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +use wasm::InternalErrorInner; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; + +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// When the payjoin directory returns an unexpected status code + UnexpectedStatusCode(http::StatusCode), + /// Internal errors that should not be pattern matched by users + #[doc(hidden)] + Internal(InternalError), +} + +macro_rules! impl_from_error { + ($from:ty, $to:ident) => { + impl From<$from> for super::Error { + fn from(value: $from) -> Self { + Self::Internal(super::InternalError(InternalErrorInner::$to(value.into()))) + } + } + }; +} +pub(crate) use impl_from_error; +#[derive(Debug)] +pub struct InternalError(InternalErrorInner); + +impl From for Error { + fn from(value: InternalError) -> Self { Self::Internal(value) } +} + +impl From for Error { + fn from(value: InternalErrorInner) -> Self { Self::Internal(InternalError(value)) } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::UnexpectedStatusCode(code) => { + write!(f, "Unexpected status code from payjoin directory: {code}") + } + Self::Internal(InternalError(e)) => e.fmt(f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Internal(InternalError(e)) => e.source(), + Self::UnexpectedStatusCode(_) => None, + } + } +} diff --git a/payjoin/src/core/io.rs b/payjoin/src/core/io/native.rs similarity index 75% rename from payjoin/src/core/io.rs rename to payjoin/src/core/io/native.rs index d335a3fce..7cb6852bf 100644 --- a/payjoin/src/core/io.rs +++ b/payjoin/src/core/io/native.rs @@ -4,6 +4,7 @@ use std::time::Duration; use http::header::ACCEPT; use reqwest::{Client, Proxy}; +use super::Error; use crate::into_url::IntoUrl; use crate::OhttpKeys; @@ -71,25 +72,11 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result for Error { - fn from(value: url::ParseError) -> Self { - Self::Internal(InternalError(InternalErrorInner::ParseUrl(value.into()))) - } -} +super::impl_from_error!(url::ParseError, ParseUrl); -macro_rules! impl_from_error { - ($from:ty, $to:ident) => { - impl From<$from> for Error { - fn from(value: $from) -> Self { - Self::Internal(InternalError(InternalErrorInner::$to(value))) - } - } - }; -} - -impl_from_error!(crate::into_url::Error, ParseUrl); -impl_from_error!(reqwest::Error, Reqwest); -impl_from_error!(std::io::Error, Io); +super::impl_from_error!(crate::into_url::Error, ParseUrl); +super::impl_from_error!(reqwest::Error, Reqwest); +super::impl_from_error!(std::io::Error, Io); #[cfg(feature = "_manual-tls")] -impl_from_error!(rustls::Error, Rustls); - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::UnexpectedStatusCode(code) => { - write!(f, "Unexpected status code from payjoin directory: {code}") - } - Self::Internal(InternalError(e)) => e.fmt(f), - } - } -} +super::impl_from_error!(rustls::Error, Rustls); impl std::fmt::Display for InternalErrorInner { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -148,15 +110,6 @@ impl std::fmt::Display for InternalErrorInner { } } -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Internal(InternalError(e)) => e.source(), - Self::UnexpectedStatusCode(_) => None, - } - } -} - impl std::error::Error for InternalErrorInner { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { use InternalErrorInner::*; @@ -172,14 +125,6 @@ impl std::error::Error for InternalErrorInner { } } -impl From for Error { - fn from(value: InternalError) -> Self { Self::Internal(value) } -} - -impl From for Error { - fn from(value: InternalErrorInner) -> Self { Self::Internal(InternalError(value)) } -} - #[cfg(test)] mod tests { use std::str::FromStr; @@ -188,6 +133,7 @@ mod tests { use reqwest::Response; use super::*; + use crate::io::InternalError; fn mock_response(status: StatusCode, body: Vec) -> Response { Response::from(http::response::Response::builder().status(status).body(body).unwrap()) diff --git a/payjoin/src/core/io/wasm.rs b/payjoin/src/core/io/wasm.rs new file mode 100644 index 000000000..5a767e0bb --- /dev/null +++ b/payjoin/src/core/io/wasm.rs @@ -0,0 +1,360 @@ +//! WASM IO implementation. + +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +use base64::engine::general_purpose; +use base64::Engine; +use futures::io::{AsyncReadExt, AsyncWriteExt}; +use futures::StreamExt; +#[cfg(feature = "_manual-tls")] +use futures_rustls::pki_types::CertificateDer; +use gloo_net::websocket::futures::WebSocket; +use js_sys::{Promise, Reflect, Uint8Array}; +use url::Url; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +use super::Error; +use crate::{into_url, IntoUrl, OhttpKeys}; + +pub async fn fetch_ohttp_keys( + ohttp_relay: impl IntoUrl, + payjoin_directory: impl IntoUrl, +) -> Result { + fetch_ohttp_keys_strategy(ohttp_relay, payjoin_directory, None).await +} + +#[cfg(feature = "_manual-tls")] +pub async fn fetch_ohttp_keys_with_cert( + ohttp_relay: impl IntoUrl, + payjoin_directory: impl IntoUrl, + cert_der: Vec, +) -> Result { + fetch_ohttp_keys_strategy(ohttp_relay, payjoin_directory, Some(cert_der)).await +} + +async fn fetch_ohttp_keys_strategy( + relay: impl IntoUrl, + directory: impl IntoUrl, + cert: Option>, +) -> Result { + let relay = relay.into_url()?; + let directory = directory.into_url()?; + let runtime = get_runtime_env(); + + // if we're not running in a browser + if !matches!(runtime, Runtime::BrowserMain | Runtime::WebWorker) { + if let Ok(keys) = fetch_via_http_connect(&relay, &directory, cert.as_deref()).await { + return Ok(keys); + } + } + + fetch_via_websocket(relay, directory, cert.as_deref()).await +} + +// Helper macro to create JS objects, we can't use serde +// complex types in JS objects. +macro_rules! js_object { + ($($key:expr => $val:expr),* $(,)?) => {{ + let obj = js_sys::Object::new(); + $( + let _ = js_sys::Reflect::set( + &obj, + &wasm_bindgen::JsValue::from($key), + &wasm_bindgen::JsValue::from($val) + ); + )* + obj + }}; +} + +#[wasm_bindgen(module = "undici")] +extern "C" { + #[wasm_bindgen(js_name = ProxyAgent)] + type ProxyAgent; + + // ProxyAgent::new() + #[wasm_bindgen(constructor, js_class = "ProxyAgent", catch)] + fn new(options: &JsValue) -> Result; + + // undici::fetch() + #[wasm_bindgen(js_name = fetch, catch)] + fn fetch(url: &str, options: &JsValue) -> Result; + +} + +#[wasm_bindgen] +extern "C" { + type Response; + + #[wasm_bindgen(method, structural, js_name = arrayBuffer)] + fn array_buffer(this: &Response) -> Promise; +} + +async fn fetch_via_http_connect( + ohttp_relay: &Url, + payjoin_directory: &Url, + cert_der: Option<&[u8]>, +) -> Result { + let directory_url = payjoin_directory.join("/.well-known/ohttp-gateway")?; + + let fetch_options = { + let request_tls = if let Some(cert) = cert_der { + js_object! { + "ca" => serialize_der_to_pem(cert) // we end up not validating this unfortunately + } + } else { + js_object! {} + }; + + let agent_options = js_object! { + "uri" => ohttp_relay.as_str(), // proxy_url, + "requestTls" => request_tls + }; + + let agent = + ProxyAgent::new(&agent_options).map_err(InternalErrorInner::ProxyFetchFailed)?; + + let headers = js_object! { + "Accept" => "application/ohttp-keys" + }; + + js_object! { + "dispatcher" => agent, + "headers" => headers + } + }; + + let ohttp_key_bytes = { + // call unidici fetch(), and jump through multiple hoops to get a Vec() out of the response + // This can fail for a multitude of reasons, including: + // 1. Invalid proxy (Some cases that could be categorized under BadUrl) + // 2. Invalid certificate (Ideally we should throw BadCert but then we'd be + // unnecesarrily parsing the cert) + // 3. Other network errors + let response = fetch(directory_url.as_str(), &fetch_options) + .map_err(InternalErrorInner::ProxyFetchFailed)?; + let response = + JsFuture::from(response).await.map_err(InternalErrorInner::ProxyFetchFailed)?; + let response: Response = response.unchecked_into(); + Uint8Array::new( + &JsFuture::from(response.array_buffer()) + .await + .map_err(|_e| InternalErrorInner::InvalidResponse)?, + ) + .to_vec() + }; + + OhttpKeys::decode(&ohttp_key_bytes).map_err(Error::from) +} + +async fn fetch_via_websocket( + ohttp_relay: Url, + payjoin_directory: Url, + _cert_der: Option<&[u8]>, +) -> Result { + let tls_connector = { + #[allow(unused_mut)] + let mut root_store = + rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; + + #[cfg(feature = "_manual-tls")] + if let Some(cert) = _cert_der { + root_store.add(CertificateDer::from_slice(cert))? + } + + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + futures_rustls::TlsConnector::from(Arc::new(config)) + }; + let directory_host = payjoin_directory + .host_str() + .ok_or_else(|| InternalErrorInner::BadUrl(url::ParseError::EmptyHost.into()))?; + let directory_port = payjoin_directory.port_or_known_default(); + + let domain = rustls::pki_types::ServerName::try_from(directory_host) + .map_err(|_| InternalErrorInner::BadUrl(url::ParseError::IdnaError.into()))? + .to_owned(); + + let ws_scheme = match ohttp_relay.scheme() { + "https" => "wss", + _ => "ws", + }; + + let relay_host = ohttp_relay + .host_str() + .ok_or_else(|| InternalErrorInner::BadUrl(url::ParseError::EmptyHost.into()))?; + + let relay_port = + ohttp_relay.port_or_known_default().unwrap_or(if ws_scheme == "wss" { 443 } else { 80 }); + + let ws_url = + format!("{}://{}:{}/{}", ws_scheme, relay_host, relay_port, payjoin_directory.as_str()); + + let ws = WebSocket::open(&ws_url).map_err(|e| InternalErrorInner::WebSocketFailed(e))?; + + let mut tls_stream = + tls_connector.connect(domain, ws).await.map_err(|_| InternalErrorInner::InvalidResponse)?; + let host_header = match directory_port { + Some(port) => format!("{}:{}", directory_host, port), + None => directory_host.to_string(), + }; + let ohttp_keys_req = + format!("GET /ohttp-keys HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n", host_header); + tls_stream + .write_all(ohttp_keys_req.as_bytes()) + .await + .map_err(|_| InternalErrorInner::InvalidResponse)?; + tls_stream.flush().await.map_err(|_| InternalErrorInner::InvalidResponse)?; + let mut response_bytes = Vec::new(); + tls_stream + .read_to_end(&mut response_bytes) + .await + .map_err(|_| InternalErrorInner::InvalidResponse)?; + + // Consume whatever remains in the WebSocket stream + // necesarry, else the stream may try to call onmessage handlers + // that have been dropped. + let (mut ws, _tls_connection) = tls_stream.into_inner(); + while let Some(_) = ws.next().await {} + + let (_headers, res_body) = separate_headers_and_body(&response_bytes)?; + OhttpKeys::decode(res_body).map_err(Error::from) +} + +#[derive(Debug)] +pub(super) enum InternalErrorInner { + BadUrl(into_url::Error), + #[cfg(feature = "_manual-tls")] + InvalidCert(rustls::Error), + WebSocketFailed(gloo_utils::errors::JsError), + ProxyFetchFailed(JsValue), + InvalidResponse, + OhttpDecodeFailed(ohttp::Error), +} + +// Convert DER-encoded certificate bytes to a PEM-formatted certificate string. +// Wrap lines at 64 characters per PEM convention. +fn serialize_der_to_pem(cert_der: &[u8]) -> String { + let b64 = general_purpose::STANDARD.encode(cert_der); + let mut pem = String::with_capacity(b64.len() + 64); + pem.push_str("-----BEGIN CERTIFICATE-----\n"); + // Insert line breaks every 64 chars + for chunk in b64.as_bytes().chunks(64) { + // Safe to unwrap: base64 output is valid ASCII + pem.push_str(std::str::from_utf8(chunk).unwrap()); + pem.push('\n'); + } + pem.push_str("-----END CERTIFICATE-----"); + pem +} + +fn separate_headers_and_body(response_bytes: &[u8]) -> Result<(&[u8], &[u8]), Error> { + let separator = b"\r\n\r\n"; + + // Search for the separator + if let Some(position) = + response_bytes.windows(separator.len()).position(|window| window == separator) + { + // The body starts immediately after the separator + let body_start_index = position + separator.len(); + let headers = &response_bytes[..position]; + let body = &response_bytes[body_start_index..]; + + Ok((headers, body)) + } else { + Err(InternalErrorInner::InvalidResponse.into()) + } +} +impl std::fmt::Display for InternalErrorInner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidResponse => write!(f, "Request failed"), + Self::BadUrl(e) => write!(f, "URL parse error: {e}"), + Self::OhttpDecodeFailed(e) => write!(f, "Failed to decode OHTTP keys: {e}"), + #[cfg(feature = "_manual-tls")] + Self::InvalidCert(e) => write!(f, "Invalid certificate: {e}"), + Self::WebSocketFailed(e) => write!(f, "WebSocket connection failed: {e}"), + Self::ProxyFetchFailed(e) => write!(f, "Proxy fetch failed: {}", js_value_to_string(e)), + } + } +} + +impl std::error::Error for InternalErrorInner { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidResponse => None, + Self::BadUrl(e) => Some(e), + Self::OhttpDecodeFailed(e) => Some(e), + #[cfg(feature = "_manual-tls")] + Self::InvalidCert(e) => Some(e), + Self::WebSocketFailed(e) => Some(e), + Self::ProxyFetchFailed(_) => None, + } + } +} + +super::impl_from_error!(into_url::Error, BadUrl); +super::impl_from_error!(url::ParseError, BadUrl); +super::impl_from_error!(ohttp::Error, OhttpDecodeFailed); +#[cfg(feature = "_manual-tls")] +super::impl_from_error!(rustls::Error, InvalidCert); + +fn js_value_to_string(value: &JsValue) -> String { + if let Some(s) = value.as_string() { + s + } else if let Some(e) = value.dyn_ref::() { + String::from(e.to_string()) + } else { + format!("{:?}", value) + } +} + +enum Runtime { + BrowserMain, + WebWorker, + NodeJS, + Unknown, +} + +impl std::fmt::Display for Runtime { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Runtime::BrowserMain => write!(f, "BrowserMain"), + Runtime::WebWorker => write!(f, "WebWorker"), + Runtime::NodeJS => write!(f, "NodeJS"), + Runtime::Unknown => write!(f, "Unknown"), + } + } +} + +fn get_runtime_env() -> Runtime { + let global = js_sys::global(); + + // Bun and Deno are checked first as they both have the process global. + if is_defined(&global, "process") { + return Runtime::NodeJS; + } + + if is_defined(&global, "window") && is_defined(&global, "document") { + return Runtime::BrowserMain; + } + + if is_defined(&global, "WorkerGlobalScope") || is_defined(&global, "importScripts") { + return Runtime::WebWorker; + } + + Runtime::Unknown +} + +fn is_defined(global: &js_sys::Object, prop: &str) -> bool { + // Using Reflect to check for properties on global, js_sys does not expose any + // web/runtime APIs. However, Reflect is part of the ECMAScript standard + match Reflect::get(global, &JsValue::from_str(prop)) { + Ok(value) => !value.is_undefined() && !value.is_null(), + Err(_) => false, + } +} From 4d956e213febbe08190115944a85e4698c5e4d27 Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 5/6] Enable compilation of io module --- payjoin-ffi/javascript/ubrn.config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payjoin-ffi/javascript/ubrn.config.yaml b/payjoin-ffi/javascript/ubrn.config.yaml index 0d1c9c389..e78482333 100644 --- a/payjoin-ffi/javascript/ubrn.config.yaml +++ b/payjoin-ffi/javascript/ubrn.config.yaml @@ -3,7 +3,7 @@ rust: manifestPath: Cargo.toml web: - features: ["wasm_js"] + features: ["wasm_js", "io", "_manual-tls"] defaultFeatures: false target: nodejs manifestPatchFile: wasm-manifest-patch.toml From 8cdca9f69401c15adff18695dc1de8021a206ae0 Mon Sep 17 00:00:00 2001 From: Anu Date: Thu, 26 Mar 2026 00:05:20 +0530 Subject: [PATCH 6/6] Add integration tests testing fetchOhttpKeysWithCert service.fetchOhttpKeysWithCert is still using NAAPI API. These test check basic functionality. --- .../javascript/test/integration.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/payjoin-ffi/javascript/test/integration.test.ts b/payjoin-ffi/javascript/test/integration.test.ts index 4153335cf..546de5bb1 100644 --- a/payjoin-ffi/javascript/test/integration.test.ts +++ b/payjoin-ffi/javascript/test/integration.test.ts @@ -702,10 +702,82 @@ async function testIntegrationV2ToV2(): Promise { assert.strictEqual(senderBalance, 0.0, "Sender balance should be 0"); } +/** Calling the WASM-exported fetchOhttpKeysWithCert exercises the wasm.rs code path. */ +async function testWasmFetchOhttpKeys(): Promise { + const services = new testUtils.TestServices(); + services.waitForServicesReady(); + const relayUrl = services.ohttpRelayUrl(); + const directoryUrl = services.directoryUrl(); + const certDer = services.cert(); + + // This calls the WASM-compiled fetch_ohttp_keys_with_cert from wasm.rs, + // passing the test services' self-signed certificate so the TLS handshake + // trusts the local directory. + let ohttpKeys; + try { + ohttpKeys = await payjoin.fetchOhttpKeysWithCert( + relayUrl, + directoryUrl, + certDer.buffer as ArrayBuffer, + ); + } catch (e: any) { + // uniffi wraps errors as UniffiThrownObject with an `inner` IoError object. + // The inner object exposes the Rust error message via .message(). + assert.fail(`fetchOhttpKeysWithCert threw: ${e?.inner?.message?.()}`); + } + assert.ok( + ohttpKeys, + "WASM fetchOhttpKeysWithCert should return a valid OhttpKeys object", + ); +} + +async function testWasmFetchOhttpKeysInvalidUrl(): Promise { + const services = new testUtils.TestServices(); + services.waitForServicesReady(); + const relayUrl = services.ohttpRelayUrl(); + const directoryUrl = "invalid-url"; + const certDer = services.cert(); + + try { + await payjoin.fetchOhttpKeysWithCert( + relayUrl, + directoryUrl, + certDer.buffer as ArrayBuffer, + ); + assert.fail("Should have thrown error"); + } catch (e: any) { + console.debug(`Expected error caught: ${e.inner.message()}`); + assert.ok(true); + } +} + +async function testWasmFetchOhttpKeysInvalidCert(): Promise { + const services = new testUtils.TestServices(); + services.waitForServicesReady(); + const relayUrl = services.ohttpRelayUrl(); + const directoryUrl = services.directoryUrl(); + const certDer = new Uint8Array([0x00]); // Invalid cert + + try { + await payjoin.fetchOhttpKeysWithCert( + relayUrl, + directoryUrl, + certDer.buffer as ArrayBuffer, + ); + assert.fail("Should have thrown error"); + } catch (e: any) { + console.debug(`Expected error caught: ${e.inner.message()}`); + assert.ok(true); + } +} + async function runTests(): Promise { await uniffiInitAsync(); testFfiValidation(); await testIntegrationV2ToV2(); + await testWasmFetchOhttpKeys(); + await testWasmFetchOhttpKeysInvalidUrl(); + await testWasmFetchOhttpKeysInvalidCert(); } runTests().catch((error: unknown) => {