From 3218c2e59d29b9531e7ff29eb4c00e53a866ecea Mon Sep 17 00:00:00 2001 From: richardanyalai Date: Tue, 3 Feb 2026 10:54:42 +0700 Subject: [PATCH 1/2] refact: made WASM compatible --- Cargo.lock | 135 +++++++++--------- Cargo.toml | 18 +-- crates/cryptography/Cargo.toml | 6 + .../cryptography/src/symmetric/aes_256_gcm.rs | 18 ++- crates/keycloak/Cargo.toml | 7 +- crates/keycloak/src/updater.rs | 38 +++-- crates/ledger/Cargo.toml | 19 ++- crates/ledger/src/lib.rs | 3 + crates/ledger/src/utils.rs | 5 +- crates/registry/Cargo.toml | 10 +- crates/wallet/Cargo.toml | 13 +- crates/wallet/src/amulet_rules.rs | 20 ++- crates/wallet/src/mining_rounds.rs | 22 ++- 13 files changed, 200 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 584a90f..7e9a497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ version = "0.2.0" dependencies = [ "aes-gcm", "base64", + "getrandom 0.2.16", ] [[package]] @@ -620,8 +621,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -767,10 +770,10 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", + "rustls 0.23.32", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", ] @@ -1088,9 +1091,9 @@ dependencies = [ "common", "dotenvy", "futures-util", + "getrandom 0.2.16", "keycloak", "log", - "rand", "reqwest", "serde", "serde_json", @@ -1118,15 +1121,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.28" @@ -1272,29 +1266,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1538,15 +1509,6 @@ dependencies = [ "getrandom 0.2.16", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -1680,6 +1642,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.32" @@ -1688,7 +1664,7 @@ checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -1702,6 +1678,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.7" @@ -1767,12 +1754,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "security-framework" version = "2.11.1" @@ -1910,15 +1891,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.11" @@ -2094,9 +2066,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2123,13 +2093,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.32", "tokio", ] @@ -2152,10 +2133,12 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "native-tls", + "rustls 0.22.4", + "rustls-pki-types", "tokio", - "tokio-native-tls", + "tokio-rustls 0.25.0", "tungstenite", + "webpki-roots 0.26.11", ] [[package]] @@ -2337,8 +2320,9 @@ dependencies = [ "http", "httparse", "log", - "native-tls", "rand", + "rustls 0.22.4", + "rustls-pki-types", "sha1", "thiserror", "url", @@ -2453,7 +2437,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-macros", ] [[package]] @@ -2571,6 +2554,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 021e170..50ccf40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [workspace] members = [ - "crates/common", - "crates/cryptography", - "crates/examples", - "crates/keycloak", - "crates/ledger", - "crates/ledgrpc", - "crates/registry", - "crates/wallet", + "crates/common", + "crates/cryptography", + "crates/examples", + "crates/keycloak", + "crates/ledger", + "crates/ledgrpc", + "crates/registry", + "crates/wallet", ] resolver = "2" @@ -16,7 +16,7 @@ edition = "2024" license = "MIT" version = "0.2.0" -[workspace.profile.release] +[profile.release] opt-level = 3 strip = "debuginfo" codegen-units = 1 diff --git a/crates/cryptography/Cargo.toml b/crates/cryptography/Cargo.toml index 57dc047..d3cd827 100644 --- a/crates/cryptography/Cargo.toml +++ b/crates/cryptography/Cargo.toml @@ -8,3 +8,9 @@ version.workspace = true base64 = { workspace = true } aes-gcm = "0.10" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +getrandom = { version = "0.2" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } diff --git a/crates/cryptography/src/symmetric/aes_256_gcm.rs b/crates/cryptography/src/symmetric/aes_256_gcm.rs index 948e6aa..d91071e 100644 --- a/crates/cryptography/src/symmetric/aes_256_gcm.rs +++ b/crates/cryptography/src/symmetric/aes_256_gcm.rs @@ -1,9 +1,16 @@ -use aes_gcm::aead::{Aead, KeyInit, OsRng}; -use aes_gcm::{AeadCore, Aes256Gcm, Nonce}; +use aes_gcm::aead::{Aead, KeyInit}; +use aes_gcm::{Aes256Gcm, Nonce}; use base64::{Engine as _, engine::general_purpose}; pub const PREFIX: &str = "aes-256-gcm"; +/// Generate a random 12-byte nonce using getrandom (WASM-compatible) +fn generate_nonce() -> Result<[u8; 12], String> { + let mut nonce = [0u8; 12]; + getrandom::getrandom(&mut nonce).map_err(|e| format!("RNG error: {}", e))?; + Ok(nonce) +} + /// Encrypt a UTF-8 string using AES-256-GCM. /// Returns a base64-encoded string containing both the nonce and ciphertext. #[allow(dead_code)] @@ -14,14 +21,15 @@ pub fn encrypt_string(key: String, plaintext: String) -> Result let cipher = Aes256Gcm::new_from_slice(&key_bytes).map_err(|e| format!("Cipher init: {}", e))?; - let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let nonce_bytes = generate_nonce()?; + let nonce = Nonce::from_slice(&nonce_bytes); let ciphertext = cipher - .encrypt(&nonce, plaintext.as_bytes()) + .encrypt(nonce, plaintext.as_bytes()) .map_err(|e| format!("Encrypt error: {}", e))?; let mut combined = Vec::with_capacity(12 + ciphertext.len()); - combined.extend_from_slice(&nonce); + combined.extend_from_slice(&nonce_bytes); combined.extend_from_slice(&ciphertext); Ok(general_purpose::STANDARD.encode(combined)) } diff --git a/crates/keycloak/Cargo.toml b/crates/keycloak/Cargo.toml index 334158f..6b6c6d1 100644 --- a/crates/keycloak/Cargo.toml +++ b/crates/keycloak/Cargo.toml @@ -6,6 +6,11 @@ version.workspace = true [dependencies] base64 = { workspace = true } -reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +reqwest = { workspace = true } diff --git a/crates/keycloak/src/updater.rs b/crates/keycloak/src/updater.rs index f229b36..afba5a7 100644 --- a/crates/keycloak/src/updater.rs +++ b/crates/keycloak/src/updater.rs @@ -57,31 +57,49 @@ where #[cfg(test)] mod tests { - #[test] - fn test_updater() { - use super::DeadlineUpdater; - use std::time::{Duration, SystemTime}; + use super::DeadlineUpdater; + use std::time::{Duration, SystemTime}; + #[test] + fn test_updater_initial_refresh() { let mut counter = 0; let mut updater = DeadlineUpdater::new(|| { counter += 1; - let next_deadline = SystemTime::now() + Duration::from_secs(2); + let next_deadline = SystemTime::now() + Duration::from_secs(60); (next_deadline, counter) }); - // Initial get should return 1 + // Initial get should return 1 (triggers refresh because starts expired) let value1 = updater.get().unwrap(); assert_eq!(value1, 1); // Immediate second get should return cached value 1 let value2 = updater.get().unwrap(); assert_eq!(value2, 1); + } + + #[test] + fn test_updater_expired_deadline() { + let mut counter = 0; + let mut updater = DeadlineUpdater::new(|| { + counter += 1; + // Set deadline in the past to force refresh on next get + let next_deadline = SystemTime::now() + .checked_sub(Duration::from_secs(1)) + .unwrap_or(SystemTime::UNIX_EPOCH); + (next_deadline, counter) + }); - // Wait for more than 2 seconds to exceed deadline - std::thread::sleep(Duration::from_secs(3)); + // First get triggers refresh + let value1 = updater.get().unwrap(); + assert_eq!(value1, 1); + + // Second get also triggers refresh because deadline is in the past + let value2 = updater.get().unwrap(); + assert_eq!(value2, 2); - // Next get should refresh and return 2 + // Third get also triggers refresh let value3 = updater.get().unwrap(); - assert_eq!(value3, 2); + assert_eq!(value3, 3); } } diff --git a/crates/ledger/Cargo.toml b/crates/ledger/Cargo.toml index 0499833..5611d04 100644 --- a/crates/ledger/Cargo.toml +++ b/crates/ledger/Cargo.toml @@ -6,22 +6,31 @@ version.workspace = true [dependencies] base64 = { workspace = true } -reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } canton-api-client = "3.3.0-0.1.0" futures-util = "0.3.31" log = "0.4.17" -rand = "0.8" -tokio-tungstenite = { version = "0.21", features = ["native-tls"] } url = "2.5.4" common = { workspace = true } +# Native target dependencies +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { workspace = true, features = ["json"] } +tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } +getrandom = { version = "0.2" } + +# WASM target dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +reqwest = { workspace = true, features = ["json"] } +getrandom = { version = "0.2", features = ["js"] } + [dev-dependencies] -dotenvy = { workspace = true } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true } tokio-macros = { workspace = true } +dotenvy = { workspace = true } + keycloak = { workspace = true } diff --git a/crates/ledger/src/lib.rs b/crates/ledger/src/lib.rs index 1d13344..eb74cd6 100644 --- a/crates/ledger/src/lib.rs +++ b/crates/ledger/src/lib.rs @@ -4,6 +4,9 @@ pub mod common; pub mod ledger_end; pub mod submit; pub mod utils; + +// WebSocket module is only available on native targets (requires tokio-tungstenite) +#[cfg(not(target_arch = "wasm32"))] pub mod websocket; pub use canton_api_client::models; diff --git a/crates/ledger/src/utils.rs b/crates/ledger/src/utils.rs index 823c266..9a867c8 100644 --- a/crates/ledger/src/utils.rs +++ b/crates/ledger/src/utils.rs @@ -1,5 +1,4 @@ use base64::{Engine as _, engine::general_purpose}; -use rand::Rng; pub(crate) fn http_to_ws(url: &str) -> String { if url.starts_with("ws://") || url.starts_with("wss://") { @@ -13,8 +12,10 @@ pub(crate) fn http_to_ws(url: &str) -> String { } } +/// Generate a random 16-byte string using getrandom (WASM-compatible) pub(crate) fn random_16_byte_string() -> String { let mut bytes = [0u8; 16]; - rand::thread_rng().fill(&mut bytes); + // getrandom works on all platforms including WASM with js feature + getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes"); general_purpose::STANDARD.encode(bytes) } diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 9178edb..4309477 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -5,16 +5,22 @@ license.workspace = true version.workspace = true [dependencies] -reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } common = { workspace = true } +# Platform-specific HTTP client +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { workspace = true, features = ["json"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +reqwest = { workspace = true, features = ["json"] } + [dev-dependencies] chrono = { workspace = true } dotenvy = { workspace = true } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } keycloak = { workspace = true } ledger = { workspace = true } diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index cafb9f9..c23670c 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -5,14 +5,19 @@ license.workspace = true version.workspace = true [dependencies] -chrono = { workspace = true, features = ["serde", "clock"] } -reqwest = { workspace = true, features = ["json"] } +chrono = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tokio-macros = { workspace = true } + +# Platform-specific HTTP client +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +reqwest = { workspace = true, features = ["json"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +reqwest = { workspace = true, features = ["json"] } [dev-dependencies] dotenvy = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } keycloak = { workspace = true } diff --git a/crates/wallet/src/amulet_rules.rs b/crates/wallet/src/amulet_rules.rs index 6409142..d78970d 100644 --- a/crates/wallet/src/amulet_rules.rs +++ b/crates/wallet/src/amulet_rules.rs @@ -1,6 +1,5 @@ use reqwest::header; use serde::{Deserialize, Serialize}; -use std::time::Duration; #[derive(Debug, Serialize, Deserialize)] pub struct AmuletRulesWrapper { @@ -29,11 +28,24 @@ pub struct Params { pub wallet_api_host: String, } -pub async fn get(params: Params) -> Result { - let client = reqwest::Client::builder() +#[cfg(not(target_arch = "wasm32"))] +fn build_client() -> Result { + use std::time::Duration; + reqwest::Client::builder() .timeout(Duration::from_secs(60)) .build() - .map_err(|err| format!("Failed to get reqwest builder: {}", err))?; + .map_err(|err| format!("Failed to get reqwest builder: {}", err)) +} + +#[cfg(target_arch = "wasm32")] +fn build_client() -> Result { + reqwest::Client::builder() + .build() + .map_err(|err| format!("Failed to get reqwest builder: {}", err)) +} + +pub async fn get(params: Params) -> Result { + let client = build_client()?; let url = format!( "{}/api/validator/v0/scan-proxy/amulet-rules", diff --git a/crates/wallet/src/mining_rounds.rs b/crates/wallet/src/mining_rounds.rs index e31b6d0..34bd5ad 100644 --- a/crates/wallet/src/mining_rounds.rs +++ b/crates/wallet/src/mining_rounds.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::time::Duration; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -218,6 +217,22 @@ pub struct IssuingMiningRoundPayload { pub issuance_per_validator_reward_coupon: String, } +#[cfg(not(target_arch = "wasm32"))] +fn build_client() -> Result { + use std::time::Duration; + reqwest::Client::builder() + .timeout(Duration::from_secs(60)) + .build() + .map_err(|err| format!("Failed to get reqwest builder: {}", err)) +} + +#[cfg(target_arch = "wasm32")] +fn build_client() -> Result { + reqwest::Client::builder() + .build() + .map_err(|err| format!("Failed to get reqwest builder: {}", err)) +} + /// GET /api/validator/v0/scan-proxy/open-and-issuing-mining-rounds /// /// `base_url` corresponds to `env.GetWalletAPI()` in the Go code. @@ -230,10 +245,7 @@ pub async fn get_open_mining_rounds( base_url.trim_end_matches('/') ); - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(60)) - .build() - .map_err(|err| format!("Failed to get reqwest builder: {}", err))?; + let client = build_client()?; let response = client .get(&url) From a5716a0d79bf3a961af9cdd27816513f806a11a3 Mon Sep 17 00:00:00 2001 From: richardanyalai Date: Tue, 3 Feb 2026 14:31:44 +0700 Subject: [PATCH 2/2] refact: tokio tungstenite wasm --- Cargo.lock | 122 ++++++++---------- crates/ledger/Cargo.toml | 14 +- crates/ledger/src/lib.rs | 3 - crates/ledger/src/utils.rs | 10 -- .../ledger/src/websocket/active_contracts.rs | 97 +++++--------- crates/ledger/src/websocket/update.rs | 53 +++----- 6 files changed, 104 insertions(+), 195 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e9a497..d97dcce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,12 +204,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.10.1" @@ -329,7 +323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -770,10 +764,10 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.32", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", ] @@ -1086,12 +1080,10 @@ dependencies = [ name = "ledger" version = "0.2.0" dependencies = [ - "base64", "canton-api-client", "common", "dotenvy", "futures-util", - "getrandom 0.2.16", "keycloak", "log", "reqwest", @@ -1099,7 +1091,7 @@ dependencies = [ "serde_json", "tokio", "tokio-macros", - "tokio-tungstenite", + "tokio-tungstenite-wasm", "url", ] @@ -1481,23 +1473,22 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1509,6 +1500,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1642,20 +1642,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.32" @@ -1664,7 +1650,7 @@ checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki", "subtle", "zeroize", ] @@ -1678,17 +1664,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.103.7" @@ -1998,18 +1973,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2093,24 +2068,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls", "tokio", ] @@ -2127,20 +2091,40 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", - "rustls 0.22.4", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tungstenite", "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-tungstenite-wasm" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e501f4c45ccd240d6ba3d5e191ef4658a7d98ac47091b25a1b0474a5e2aacfd9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "rustls", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -2310,22 +2294,20 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", "rand", - "rustls 0.22.4", + "rustls", "rustls-pki-types", "sha1", "thiserror", - "url", "utf-8", ] diff --git a/crates/ledger/Cargo.toml b/crates/ledger/Cargo.toml index 5611d04..8ed9eb2 100644 --- a/crates/ledger/Cargo.toml +++ b/crates/ledger/Cargo.toml @@ -5,7 +5,6 @@ license.workspace = true version.workspace = true [dependencies] -base64 = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -13,19 +12,10 @@ canton-api-client = "3.3.0-0.1.0" futures-util = "0.3.31" log = "0.4.17" url = "2.5.4" - -common = { workspace = true } - -# Native target dependencies -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] reqwest = { workspace = true, features = ["json"] } -tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } -getrandom = { version = "0.2" } +tokio-tungstenite-wasm = { version = "0.8", features = ["rustls-tls-webpki-roots"] } -# WASM target dependencies -[target.'cfg(target_arch = "wasm32")'.dependencies] -reqwest = { workspace = true, features = ["json"] } -getrandom = { version = "0.2", features = ["js"] } +common = { workspace = true } [dev-dependencies] tokio = { workspace = true } diff --git a/crates/ledger/src/lib.rs b/crates/ledger/src/lib.rs index eb74cd6..1d13344 100644 --- a/crates/ledger/src/lib.rs +++ b/crates/ledger/src/lib.rs @@ -4,9 +4,6 @@ pub mod common; pub mod ledger_end; pub mod submit; pub mod utils; - -// WebSocket module is only available on native targets (requires tokio-tungstenite) -#[cfg(not(target_arch = "wasm32"))] pub mod websocket; pub use canton_api_client::models; diff --git a/crates/ledger/src/utils.rs b/crates/ledger/src/utils.rs index 9a867c8..e385cdb 100644 --- a/crates/ledger/src/utils.rs +++ b/crates/ledger/src/utils.rs @@ -1,5 +1,3 @@ -use base64::{Engine as _, engine::general_purpose}; - pub(crate) fn http_to_ws(url: &str) -> String { if url.starts_with("ws://") || url.starts_with("wss://") { url.to_string() @@ -11,11 +9,3 @@ pub(crate) fn http_to_ws(url: &str) -> String { url.to_string() } } - -/// Generate a random 16-byte string using getrandom (WASM-compatible) -pub(crate) fn random_16_byte_string() -> String { - let mut bytes = [0u8; 16]; - // getrandom works on all platforms including WASM with js feature - getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes"); - general_purpose::STANDARD.encode(bytes) -} diff --git a/crates/ledger/src/websocket/active_contracts.rs b/crates/ledger/src/websocket/active_contracts.rs index 5b7664d..cd19eaa 100644 --- a/crates/ledger/src/websocket/active_contracts.rs +++ b/crates/ledger/src/websocket/active_contracts.rs @@ -3,8 +3,7 @@ use canton_api_client::models; use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use tokio_tungstenite::tungstenite::Message; -use tokio_tungstenite::tungstenite::handshake::client::Request; +use tokio_tungstenite_wasm::{connect_with_protocols, Message}; #[derive(Debug, Clone)] pub struct Params { @@ -40,25 +39,11 @@ where ws_host.trim_end_matches('/') ); - // Parse URL to extract host - let parsed_url = url::Url::parse(&ws_url).map_err(|e| format!("Invalid URL: {e}"))?; - let host = parsed_url - .host_str() - .ok_or_else(|| "Could not extract host from URL".to_string())?; - - let protocol_header = format!("jwt.token.{}, daml.ws.auth", params.access_token); - let request = Request::builder() - .uri(parsed_url.as_str()) - .header("Sec-WebSocket-Protocol", protocol_header) - .header("Sec-WebSocket-Key", utils::random_16_byte_string()) - .header("Sec-WebSocket-Version", "13") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .header("Host", host) - .body(()) - .map_err(|e| format!("Failed to build request: {e}"))?; - - let (ws_stream, _) = tokio_tungstenite::connect_async(request) + // Authentication via Sec-WebSocket-Protocol header + let protocol = format!("jwt.token.{}", params.access_token); + let protocols = [protocol.as_str(), "daml.ws.auth"]; + + let ws_stream = connect_with_protocols(&ws_url, &protocols) .await .map_err(|e| format!("WebSocket connection error: {e}"))?; @@ -90,7 +75,7 @@ where // Send messages if needed match write - .send(Message::Text(event.to_string())) + .send(Message::text(event.to_string())) .await .map_err(|e| format!("Error sending message: {e}")) { @@ -102,7 +87,11 @@ where while let Some(message) = read.next().await { match message { - Ok(Message::Text(text)) => { + Ok(msg) if msg.is_text() => { + let text = msg + .into_text() + .map_err(|e| format!("Error reading text message: {e}"))? + .to_string(); if text.contains("A security-sensitive error has been received") { error = Some(format!( "Received security-sensitive error from server: {}", @@ -112,24 +101,19 @@ where } callback(text).await; } - Ok(Message::Binary(_)) => { + Ok(msg) if msg.is_binary() => { log::info!("Received unhandled binary message."); } - Ok(Message::Close(_)) => { + Ok(msg) if msg.is_close() => { break; } Err(e) => { error = Some(format!("WebSocket error: {e}")); break; } - msg => match msg { - Ok(other) => { - log::info!("Received other type of message: {:?}", other); - } - Err(e) => { - log::error!("Error receiving message: {}", e); - } - }, + Ok(other) => { + log::info!("Received other type of message: {:?}", other); + } } } @@ -158,25 +142,11 @@ pub async fn get(params: Params) -> Result, String ws_host.trim_end_matches('/') ); - // Parse URL to extract host - let parsed_url = url::Url::parse(&ws_url).map_err(|e| format!("Invalid URL: {e}"))?; - let host = parsed_url - .host_str() - .ok_or_else(|| "Could not extract host from URL".to_string())?; - - let protocol_header = format!("jwt.token.{}, daml.ws.auth", params.access_token); - let request = Request::builder() - .uri(parsed_url.as_str()) - .header("Sec-WebSocket-Protocol", protocol_header) - .header("Sec-WebSocket-Key", utils::random_16_byte_string()) - .header("Sec-WebSocket-Version", "13") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .header("Host", host) - .body(()) - .map_err(|e| format!("Failed to build request: {e}"))?; - - let (ws_stream, _) = tokio_tungstenite::connect_async(request) + // Authentication via Sec-WebSocket-Protocol header + let protocol = format!("jwt.token.{}", params.access_token); + let protocols = [protocol.as_str(), "daml.ws.auth"]; + + let ws_stream = connect_with_protocols(&ws_url, &protocols) .await .map_err(|e| format!("WebSocket connection error: {e}"))?; @@ -208,7 +178,7 @@ pub async fn get(params: Params) -> Result, String // Send messages if needed match write - .send(Message::Text(event.to_string())) + .send(Message::text(event.to_string())) .await .map_err(|e| format!("Error sending message: {e}")) { @@ -221,7 +191,11 @@ pub async fn get(params: Params) -> Result, String let mut result: Vec = Vec::new(); while let Some(message) = read.next().await { match message { - Ok(Message::Text(text)) => { + Ok(msg) if msg.is_text() => { + let text = msg + .into_text() + .map_err(|e| format!("Error reading text message: {e}"))? + .to_string(); if text.contains("A security-sensitive error has been received") { error = Some(format!( "Received security-sensitive error from server: {}", @@ -236,24 +210,19 @@ pub async fn get(params: Params) -> Result, String result.push(ce.js_active_contract); } } - Ok(Message::Binary(_)) => { + Ok(msg) if msg.is_binary() => { log::warn!("Received unhandled binary message."); } - Ok(Message::Close(_)) => { + Ok(msg) if msg.is_close() => { break; } Err(e) => { error = Some(format!("WebSocket error: {e}")); break; } - msg => match msg { - Ok(other) => { - log::info!("Received other type of message: {:?}", other); - } - Err(e) => { - log::error!("Error receiving message: {}", e); - } - }, + Ok(other) => { + log::info!("Received other type of message: {:?}", other); + } } } diff --git a/crates/ledger/src/websocket/update.rs b/crates/ledger/src/websocket/update.rs index c2a4a0b..bf525ea 100644 --- a/crates/ledger/src/websocket/update.rs +++ b/crates/ledger/src/websocket/update.rs @@ -1,6 +1,6 @@ use crate::{common, utils}; use futures_util::{SinkExt, StreamExt}; -use tokio_tungstenite::tungstenite::{handshake::client::Request, protocol::Message}; +use tokio_tungstenite_wasm::{connect_with_protocols, Message}; pub struct Params { pub ledger_host: String, @@ -17,25 +17,11 @@ pub async fn subscribe( let ws_host = utils::http_to_ws(¶ms.ledger_host.clone()); let ws_url = format!("{}/v2/updates", ws_host.trim_end_matches('/')); - // Parse URL to extract host - let parsed_url = url::Url::parse(&ws_url).map_err(|e| format!("Invalid URL: {e}"))?; - let host = parsed_url - .host_str() - .ok_or_else(|| "Could not extract host from URL".to_string())?; - - let protocol_header = format!("jwt.token.{}, daml.ws.auth", params.access_token); - let request = Request::builder() - .uri(parsed_url.as_str()) - .header("Sec-WebSocket-Protocol", protocol_header) - .header("Sec-WebSocket-Key", utils::random_16_byte_string()) - .header("Sec-WebSocket-Version", "13") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .header("Host", host) - .body(()) - .map_err(|e| format!("Failed to build request: {e}"))?; - - let (ws_stream, _) = tokio_tungstenite::connect_async(request) + // Authentication via Sec-WebSocket-Protocol header + let protocol = format!("jwt.token.{}", params.access_token); + let protocols = [protocol.as_str(), "daml.ws.auth"]; + + let ws_stream = connect_with_protocols(&ws_url, &protocols) .await .map_err(|e| format!("WebSocket connection error: {e}"))?; @@ -64,7 +50,7 @@ pub async fn subscribe( // Send messages if needed match write - .send(Message::Text(event.to_string())) + .send(Message::text(event.to_string())) .await .map_err(|e| format!("Error sending message: {e}")) { @@ -76,32 +62,27 @@ pub async fn subscribe( while let Some(message) = read.next().await { match message { - Ok(Message::Text(text)) => { + Ok(msg) if msg.is_text() => { + let text = msg + .into_text() + .map_err(|e| format!("Error reading text message: {e}"))? + .to_string(); if let Err(e) = message_handler(text) { log::error!("Error handling message: {e}"); } } - Ok(Message::Binary(_)) => { + Ok(msg) if msg.is_binary() => { log::info!("Received unhandled binary message."); } - Ok(Message::Ping(data)) => { - // tungstenite usually auto-pongs, but it's fine to be explicit: - let _ = write.send(Message::Pong(data)).await; - } - Ok(Message::Close(_)) => { + Ok(msg) if msg.is_close() => { return Ok(()); } Err(e) => { return Err(format!("WebSocket error: {e}")); } - msg => match msg { - Ok(other) => { - log::info!("Received other type of message: {:?}", other); - } - Err(e) => { - log::error!("Error receiving message: {}", e); - } - }, + Ok(other) => { + log::info!("Received other type of message: {:?}", other); + } } } match write