Implement native and WASM WebSocket client for sigsocket communication
- Added `NativeClient` for non-WASM environments with automatic reconnection and message handling. - Introduced `WasmClient` for WASM environments, supporting WebSocket communication and reconnection logic. - Created protocol definitions for `SignRequest` and `SignResponse` with serialization and deserialization. - Developed integration tests for the client functionality and sign request handling. - Implemented WASM-specific tests to ensure compatibility and functionality in browser environments.
This commit is contained in:
parent
b0d0aaa53d
commit
9f143ded9d
@ -5,5 +5,5 @@ members = [
|
||||
"vault",
|
||||
"evm_client",
|
||||
"wasm_app",
|
||||
"sigsocket_client",
|
||||
]
|
||||
|
||||
|
53
sigsocket_client/Cargo.toml
Normal file
53
sigsocket_client/Cargo.toml
Normal file
@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "sigsocket_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WebSocket client for sigsocket server with WASM-first support"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://git.ourworld.tf/samehabouelsaad/sal-modular"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Core dependencies (both native and WASM)
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
hex = "0.4"
|
||||
base64 = "0.21"
|
||||
url = "2.5"
|
||||
async-trait = "0.1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Native-only dependencies
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-tungstenite = "0.21"
|
||||
futures-util = "0.3"
|
||||
thiserror = "1.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# WASM-only dependencies
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"WebSocket",
|
||||
"MessageEvent",
|
||||
"Event",
|
||||
"BinaryType",
|
||||
"CloseEvent",
|
||||
"ErrorEvent",
|
||||
"Window",
|
||||
] }
|
||||
js-sys = "0.3"
|
||||
|
||||
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
env_logger = "0.10"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
214
sigsocket_client/IMPLEMENTATION.md
Normal file
214
sigsocket_client/IMPLEMENTATION.md
Normal file
@ -0,0 +1,214 @@
|
||||
# SigSocket Client Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of the `sigsocket_client` crate, a WebSocket client library designed for connecting to sigsocket servers with **WASM-first support**.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Design Principles
|
||||
|
||||
1. **WASM-First**: Designed primarily for browser environments with native support as a secondary target
|
||||
2. **No Signing Logic**: The client delegates all signing operations to the application
|
||||
3. **User Approval Flow**: Applications are notified about incoming requests and handle user approval
|
||||
4. **Protocol Compatibility**: Fully compatible with the sigsocket server protocol
|
||||
5. **Async/Await**: Modern async Rust API throughout
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
sigsocket_client/
|
||||
├── src/
|
||||
│ ├── lib.rs # Main library entry point
|
||||
│ ├── error.rs # Error types (native + WASM versions)
|
||||
│ ├── protocol.rs # Protocol message definitions
|
||||
│ ├── client.rs # Main client interface
|
||||
│ ├── native.rs # Native (tokio) implementation
|
||||
│ └── wasm.rs # WASM (web-sys) implementation
|
||||
├── examples/
|
||||
│ ├── basic_usage.rs # Native usage example
|
||||
│ └── wasm_usage.rs # WASM usage example
|
||||
├── tests/
|
||||
│ └── integration_test.rs
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Protocol Implementation
|
||||
|
||||
The sigsocket protocol is simple and consists of three message types:
|
||||
|
||||
### 1. Introduction Message
|
||||
When connecting, the client sends its public key as a hex-encoded string:
|
||||
```
|
||||
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
|
||||
```
|
||||
|
||||
### 2. Sign Request (Server → Client)
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sign Response (Client → Server)
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features Implemented
|
||||
|
||||
### ✅ Dual Platform Support
|
||||
- **Native**: Uses `tokio` and `tokio-tungstenite` for async WebSocket communication
|
||||
- **WASM**: Uses `web-sys` and `wasm-bindgen` for browser WebSocket API
|
||||
|
||||
### ✅ Type-Safe Protocol
|
||||
- `SignRequest` and `SignResponse` structs with serde serialization
|
||||
- Helper methods for base64 encoding/decoding
|
||||
- Comprehensive error handling
|
||||
|
||||
### ✅ Flexible Sign Handler Interface
|
||||
```rust
|
||||
trait SignRequestHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Connection Management
|
||||
- Automatic connection state tracking
|
||||
- Clean disconnect handling
|
||||
- Connection status queries
|
||||
|
||||
### ✅ Error Handling
|
||||
- Comprehensive error types for different failure modes
|
||||
- Platform-specific error conversions
|
||||
- WASM-compatible error handling (no `std::error::Error` dependency)
|
||||
|
||||
## Platform-Specific Implementations
|
||||
|
||||
### Native Implementation (`native.rs`)
|
||||
- Uses `tokio-tungstenite` for WebSocket communication
|
||||
- Spawns separate tasks for reading and writing
|
||||
- Thread-safe with `Arc<RwLock<T>>` for shared state
|
||||
- Supports `Send + Sync` trait bounds
|
||||
|
||||
### WASM Implementation (`wasm.rs`)
|
||||
- Uses `web-sys::WebSocket` for browser WebSocket API
|
||||
- Event-driven with JavaScript closures
|
||||
- Single-threaded (no `Send + Sync` requirements)
|
||||
- Browser console logging for debugging
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Native Usage
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let public_key = hex::decode("02f9308a...")?;
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
|
||||
client.set_sign_handler(MySignHandler);
|
||||
client.connect().await?;
|
||||
|
||||
// Client handles requests automatically
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### WASM Usage
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||
let public_key = get_user_public_key()?;
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
|
||||
client.set_sign_handler(WasmSignHandler);
|
||||
client.connect().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- Protocol message serialization/deserialization
|
||||
- Error handling and conversion
|
||||
- Client creation and configuration
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end usage patterns
|
||||
- Sign request/response cycles
|
||||
- Error scenarios
|
||||
|
||||
### Documentation Tests
|
||||
- Example code in documentation is verified to compile
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies (Both Platforms)
|
||||
- `serde` + `serde_json` - JSON serialization
|
||||
- `hex` - Hex encoding/decoding
|
||||
- `base64` - Base64 encoding/decoding
|
||||
- `url` - URL parsing and validation
|
||||
|
||||
### Native-Only Dependencies
|
||||
- `tokio` - Async runtime
|
||||
- `tokio-tungstenite` - WebSocket client
|
||||
- `futures-util` - Stream utilities
|
||||
- `thiserror` - Error derive macros
|
||||
|
||||
### WASM-Only Dependencies
|
||||
- `wasm-bindgen` - Rust/JavaScript interop
|
||||
- `web-sys` - Browser API bindings
|
||||
- `js-sys` - JavaScript type bindings
|
||||
- `wasm-bindgen-futures` - Async support
|
||||
|
||||
## Build Targets
|
||||
|
||||
### Native Build
|
||||
```bash
|
||||
cargo build --features native
|
||||
cargo test --features native
|
||||
cargo run --example basic_usage --features native
|
||||
```
|
||||
|
||||
### WASM Build
|
||||
```bash
|
||||
cargo check --target wasm32-unknown-unknown --features wasm
|
||||
wasm-pack build --target web --features wasm
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
1. **Reconnection Logic**: Automatic reconnection with exponential backoff
|
||||
2. **Request Queuing**: Queue multiple concurrent sign requests
|
||||
3. **Timeout Handling**: Configurable timeouts for requests
|
||||
4. **Metrics**: Connection and request metrics
|
||||
5. **Logging**: Structured logging with configurable levels
|
||||
|
||||
### WASM Enhancements
|
||||
1. **Better Callback System**: More ergonomic callback handling in WASM
|
||||
2. **Browser Wallet Integration**: Direct integration with MetaMask, etc.
|
||||
3. **Service Worker Support**: Background request handling
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **No Private Key Storage**: The client never handles private keys
|
||||
2. **User Approval Required**: All signing requires explicit user approval
|
||||
3. **Message Validation**: All incoming messages are validated
|
||||
4. **Secure Transport**: Requires WebSocket Secure (WSS) in production
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Rust Version**: 1.70+
|
||||
- **WASM Target**: `wasm32-unknown-unknown`
|
||||
- **Browser Support**: Modern browsers with WebSocket support
|
||||
- **Server Compatibility**: Compatible with sigsocket server protocol
|
||||
|
||||
This implementation provides a solid foundation for applications that need to connect to sigsocket servers while maintaining security and user control over signing operations.
|
218
sigsocket_client/README.md
Normal file
218
sigsocket_client/README.md
Normal file
@ -0,0 +1,218 @@
|
||||
# SigSocket Client
|
||||
|
||||
A WebSocket client library for connecting to sigsocket servers with **WASM-first support**.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 **WASM-first design**: Optimized for browser environments
|
||||
- 🖥️ **Native support**: Works in native Rust applications
|
||||
- 🔐 **No signing logic**: Delegates signing to the application
|
||||
- 👤 **User approval flow**: Notifies applications about incoming requests
|
||||
- 🔌 **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||
- 🚀 **Async/await**: Modern async Rust API
|
||||
- 🔄 **Automatic reconnection**: Both platforms support reconnection with exponential backoff
|
||||
- ⏱️ **Connection timeouts**: Proper timeout handling and connection management
|
||||
- 🛡️ **Production ready**: Comprehensive error handling and reliability features
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Native Usage
|
||||
|
||||
```rust
|
||||
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||
|
||||
struct MySignHandler;
|
||||
|
||||
impl SignRequestHandler for MySignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
// 1. Present request to user
|
||||
println!("Sign request: {}", request.message);
|
||||
|
||||
// 2. Get user approval
|
||||
// ... your UI logic here ...
|
||||
|
||||
// 3. Sign the message (using your signing logic)
|
||||
let signature = your_signing_function(&request.message_bytes()?)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Your public key bytes
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")?;
|
||||
|
||||
// Create and configure client
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
client.set_sign_handler(MySignHandler);
|
||||
|
||||
// Connect and handle requests
|
||||
client.connect().await?;
|
||||
|
||||
// Client will automatically handle incoming signature requests
|
||||
// Keep the connection alive...
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### WASM Usage
|
||||
|
||||
```rust
|
||||
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
struct WasmSignHandler;
|
||||
|
||||
impl SignRequestHandler for WasmSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
// Show request to user in browser
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.alert_with_message(&format!("Sign request: {}", request.id))
|
||||
.unwrap();
|
||||
|
||||
// Your signing logic here...
|
||||
let signature = sign_with_browser_wallet(&request.message_bytes()?)?;
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||
let public_key = get_user_public_key()?;
|
||||
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
client.set_sign_handler(WasmSignHandler);
|
||||
|
||||
client.connect().await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
The sigsocket client implements a simple WebSocket protocol:
|
||||
|
||||
### 1. Introduction
|
||||
Upon connection, the client sends its public key as a hex-encoded string:
|
||||
```
|
||||
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388
|
||||
```
|
||||
|
||||
### 2. Sign Requests
|
||||
The server sends signature requests as JSON:
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sign Responses
|
||||
The client responds with signatures as JSON:
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SigSocketClient`
|
||||
|
||||
Main client for connecting to sigsocket servers.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `new(url, public_key)` - Create a new client
|
||||
- `set_sign_handler(handler)` - Set the signature request handler
|
||||
- `connect()` - Connect to the server with automatic reconnection
|
||||
- `disconnect()` - Disconnect from the server
|
||||
- `send_sign_response(response)` - Manually send a signature response
|
||||
- `state()` - Get current connection state
|
||||
- `is_connected()` - Check if connected
|
||||
|
||||
#### Reconnection Configuration (WASM only)
|
||||
|
||||
- `set_auto_reconnect(enabled)` - Enable/disable automatic reconnection
|
||||
- `set_reconnect_config(max_attempts, initial_delay_ms)` - Configure reconnection parameters
|
||||
|
||||
**Default settings:**
|
||||
- Max attempts: 5
|
||||
- Initial delay: 1000ms (with exponential backoff: 1s, 2s, 4s, 8s, 16s)
|
||||
- Auto-reconnect: enabled
|
||||
|
||||
### `SignRequestHandler` Trait
|
||||
|
||||
Implement this trait to handle incoming signature requests.
|
||||
|
||||
```rust
|
||||
trait SignRequestHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
```
|
||||
|
||||
### `SignRequest`
|
||||
|
||||
Represents a signature request from the server.
|
||||
|
||||
#### Fields
|
||||
- `id: String` - Unique request identifier
|
||||
- `message: String` - Base64-encoded message to sign
|
||||
|
||||
#### Methods
|
||||
- `message_bytes()` - Decode message to bytes
|
||||
- `message_hex()` - Get message as hex string
|
||||
|
||||
### `SignResponse`
|
||||
|
||||
Represents a signature response to send to the server.
|
||||
|
||||
#### Methods
|
||||
- `new(id, message, signature)` - Create a new response
|
||||
- `from_request_and_signature(request, signature)` - Create from request and signature bytes
|
||||
|
||||
## Examples
|
||||
|
||||
Run the basic example:
|
||||
|
||||
```bash
|
||||
cargo run --example basic_usage
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Native Build
|
||||
```bash
|
||||
cargo build
|
||||
cargo test
|
||||
cargo run --example basic_usage
|
||||
```
|
||||
|
||||
### WASM Build
|
||||
```bash
|
||||
wasm-pack build --target web
|
||||
wasm-pack test --headless --firefox # Run WASM tests
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Native
|
||||
- Rust 1.70+
|
||||
- tokio runtime
|
||||
|
||||
### WASM
|
||||
- wasm-pack
|
||||
- Modern browser with WebSocket support
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
133
sigsocket_client/examples/basic_usage.rs
Normal file
133
sigsocket_client/examples/basic_usage.rs
Normal file
@ -0,0 +1,133 @@
|
||||
//! Basic usage example for sigsocket_client
|
||||
//!
|
||||
//! This example demonstrates how to:
|
||||
//! 1. Create a sigsocket client
|
||||
//! 2. Set up a sign request handler
|
||||
//! 3. Connect to a sigsocket server
|
||||
//! 4. Handle incoming signature requests
|
||||
//!
|
||||
//! This example only runs on native (non-WASM) targets.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Example sign request handler
|
||||
///
|
||||
/// In a real application, this would:
|
||||
/// - Present the request to the user
|
||||
/// - Get user approval
|
||||
/// - Use a secure signing method (hardware wallet, etc.)
|
||||
/// - Return the signature
|
||||
struct ExampleSignHandler;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SignRequestHandler for ExampleSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
println!("📝 Received sign request:");
|
||||
println!(" ID: {}", request.id);
|
||||
println!(" Message (base64): {}", request.message);
|
||||
|
||||
// Decode the message to show what we're signing
|
||||
match request.message_bytes() {
|
||||
Ok(message_bytes) => {
|
||||
println!(" Message (hex): {}", hex::encode(&message_bytes));
|
||||
println!(" Message (text): {}", String::from_utf8_lossy(&message_bytes));
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ⚠️ Failed to decode message: {}", e);
|
||||
return Err(SigSocketError::Base64(e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Show this to the user
|
||||
// 2. Get user approval
|
||||
// 3. Sign the message using a secure method
|
||||
|
||||
println!("🤔 Would you like to sign this message? (This is a simulation)");
|
||||
println!("✅ Auto-approving for demo purposes...");
|
||||
|
||||
// Simulate signing - in reality, this would be a real signature
|
||||
let fake_signature = format!("fake_signature_for_{}", request.id);
|
||||
Ok(fake_signature.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
println!("🚀 SigSocket Client Example");
|
||||
println!("============================");
|
||||
|
||||
// Example public key (in a real app, this would be your actual public key)
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")
|
||||
.expect("Invalid public key hex");
|
||||
|
||||
println!("🔑 Public key: {}", hex::encode(&public_key));
|
||||
|
||||
// Create the client
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
println!("📡 Created client for: {}", client.url());
|
||||
|
||||
// Set up the sign request handler
|
||||
client.set_sign_handler(ExampleSignHandler);
|
||||
println!("✅ Sign request handler configured");
|
||||
|
||||
// Connect to the server
|
||||
println!("🔌 Connecting to sigsocket server...");
|
||||
match client.connect().await {
|
||||
Ok(()) => {
|
||||
println!("✅ Connected successfully!");
|
||||
println!("📊 Connection state: {:?}", client.state());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to connect: {}", e);
|
||||
println!("💡 Make sure the sigsocket server is running on localhost:8080");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the connection alive and handle requests
|
||||
println!("👂 Listening for signature requests...");
|
||||
println!(" (Press Ctrl+C to exit)");
|
||||
|
||||
// In a real application, you might want to:
|
||||
// - Handle reconnection
|
||||
// - Provide a UI for user interaction
|
||||
// - Manage multiple concurrent requests
|
||||
// - Store and manage signatures
|
||||
|
||||
// For this example, we'll just wait
|
||||
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl-c");
|
||||
|
||||
println!("\n🛑 Shutting down...");
|
||||
client.disconnect().await?;
|
||||
println!("✅ Disconnected cleanly");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Example of how you might manually send a response (if needed)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(dead_code)]
|
||||
async fn send_manual_response(client: &SigSocketClient) -> Result<()> {
|
||||
let response = SignResponse::new(
|
||||
"example-request-id",
|
||||
"dGVzdCBtZXNzYWdl", // "test message" in base64
|
||||
"ZmFrZV9zaWduYXR1cmU=", // "fake_signature" in base64
|
||||
);
|
||||
|
||||
client.send_sign_response(&response).await?;
|
||||
println!("📤 Sent manual response: {}", response.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// WASM main function (does nothing since this example is native-only)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
// This example is designed for native use only
|
||||
}
|
224
sigsocket_client/src/client.rs
Normal file
224
sigsocket_client/src/client.rs
Normal file
@ -0,0 +1,224 @@
|
||||
//! Main client interface for sigsocket communication
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec, boxed::Box};
|
||||
|
||||
use crate::{SignRequest, SignResponse, Result, SigSocketError};
|
||||
|
||||
/// Connection state of the sigsocket client
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
/// Client is disconnected
|
||||
Disconnected,
|
||||
/// Client is connecting
|
||||
Connecting,
|
||||
/// Client is connected and ready
|
||||
Connected,
|
||||
/// Client connection failed
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Trait for handling sign requests from the sigsocket server
|
||||
///
|
||||
/// Applications should implement this trait to handle incoming signature requests.
|
||||
/// The implementation should:
|
||||
/// 1. Present the request to the user
|
||||
/// 2. Get user approval
|
||||
/// 3. Sign the message (using external signing logic)
|
||||
/// 4. Return the signature
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub trait SignRequestHandler: Send + Sync {
|
||||
/// Handle a sign request from the server
|
||||
///
|
||||
/// This method is called when the server sends a signature request.
|
||||
/// The implementation should:
|
||||
/// - Decode and validate the message
|
||||
/// - Present it to the user for approval
|
||||
/// - If approved, sign the message and return the signature
|
||||
/// - If rejected, return an error
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - The sign request from the server
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(signature_bytes)` - The signature as raw bytes
|
||||
/// * `Err(error)` - If the request was rejected or signing failed
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// WASM version of SignRequestHandler (no Send + Sync requirements)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait SignRequestHandler {
|
||||
/// Handle a sign request from the server
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// Main sigsocket client
|
||||
///
|
||||
/// This is the primary interface for connecting to sigsocket servers.
|
||||
/// It handles the WebSocket connection, protocol communication, and
|
||||
/// delegates signing requests to the application.
|
||||
pub struct SigSocketClient {
|
||||
/// WebSocket server URL
|
||||
url: String,
|
||||
/// Client's public key (hex-encoded)
|
||||
public_key: Vec<u8>,
|
||||
/// Current connection state
|
||||
state: ConnectionState,
|
||||
/// Sign request handler
|
||||
sign_handler: Option<Box<dyn SignRequestHandler>>,
|
||||
/// Platform-specific implementation
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
inner: Option<crate::native::NativeClient>,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
inner: Option<crate::wasm::WasmClient>,
|
||||
}
|
||||
|
||||
impl SigSocketClient {
|
||||
/// Create a new sigsocket client
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
/// * `public_key` - Client's public key as bytes
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(client)` - New client instance
|
||||
/// * `Err(error)` - If the URL is invalid or public key is invalid
|
||||
pub fn new(url: impl Into<String>, public_key: Vec<u8>) -> Result<Self> {
|
||||
let url = url.into();
|
||||
|
||||
// Validate URL
|
||||
let _ = url::Url::parse(&url)?;
|
||||
|
||||
// Validate public key (should be 33 bytes for compressed secp256k1)
|
||||
if public_key.is_empty() {
|
||||
return Err(SigSocketError::InvalidPublicKey("Public key cannot be empty".into()));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
public_key,
|
||||
state: ConnectionState::Disconnected,
|
||||
sign_handler: None,
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler
|
||||
///
|
||||
/// This handler will be called whenever the server sends a signature request.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `handler` - Implementation of SignRequestHandler trait
|
||||
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||
where
|
||||
H: SignRequestHandler + 'static,
|
||||
{
|
||||
self.sign_handler = Some(Box::new(handler));
|
||||
}
|
||||
|
||||
/// Get the current connection state
|
||||
pub fn state(&self) -> ConnectionState {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Check if the client is connected
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.state == ConnectionState::Connected
|
||||
}
|
||||
|
||||
/// Get the client's public key as hex string
|
||||
pub fn public_key_hex(&self) -> String {
|
||||
hex::encode(&self.public_key)
|
||||
}
|
||||
|
||||
/// Get the WebSocket server URL
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-specific implementations will be added in separate modules
|
||||
impl SigSocketClient {
|
||||
/// Connect to the sigsocket server
|
||||
///
|
||||
/// This establishes a WebSocket connection and sends the introduction message
|
||||
/// with the client's public key.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully connected
|
||||
/// * `Err(error)` - Connection failed
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
if self.state == ConnectionState::Connected {
|
||||
return Err(SigSocketError::AlreadyConnected);
|
||||
}
|
||||
|
||||
self.state = ConnectionState::Connecting;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let mut client = crate::native::NativeClient::new(&self.url, &self.public_key)?;
|
||||
if let Some(handler) = self.sign_handler.take() {
|
||||
client.set_sign_handler_boxed(handler);
|
||||
}
|
||||
client.connect().await?;
|
||||
self.inner = Some(client);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let mut client = crate::wasm::WasmClient::new(&self.url, &self.public_key)?;
|
||||
if let Some(handler) = self.sign_handler.take() {
|
||||
client.set_sign_handler_boxed(handler);
|
||||
}
|
||||
client.connect().await?;
|
||||
self.inner = Some(client);
|
||||
}
|
||||
|
||||
self.state = ConnectionState::Connected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect from the sigsocket server
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully disconnected
|
||||
/// * `Err(error)` - Disconnect failed
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(inner) = &mut self.inner {
|
||||
inner.disconnect().await?;
|
||||
}
|
||||
self.inner = None;
|
||||
self.state = ConnectionState::Disconnected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
///
|
||||
/// This is typically called after the user has approved a signature request
|
||||
/// and the application has generated the signature.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `response` - The sign response containing the signature
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Response sent successfully
|
||||
/// * `Err(error)` - Failed to send response
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if !self.is_connected() {
|
||||
return Err(SigSocketError::NotConnected);
|
||||
}
|
||||
|
||||
if let Some(inner) = &self.inner {
|
||||
inner.send_sign_response(response).await
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SigSocketClient {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup will be handled by the platform-specific implementations
|
||||
}
|
||||
}
|
168
sigsocket_client/src/error.rs
Normal file
168
sigsocket_client/src/error.rs
Normal file
@ -0,0 +1,168 @@
|
||||
//! Error types for the sigsocket client
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::{String, ToString}, format};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use core::fmt;
|
||||
|
||||
/// Result type alias for sigsocket client operations
|
||||
pub type Result<T> = core::result::Result<T, SigSocketError>;
|
||||
|
||||
/// Error types that can occur when using the sigsocket client
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SigSocketError {
|
||||
/// WebSocket connection error
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
/// WebSocket protocol error
|
||||
#[error("Protocol error: {0}")]
|
||||
Protocol(String),
|
||||
|
||||
/// Message serialization/deserialization error
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Invalid public key format
|
||||
#[error("Invalid public key: {0}")]
|
||||
InvalidPublicKey(String),
|
||||
|
||||
/// Invalid URL format
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
/// Client is not connected
|
||||
#[error("Client is not connected")]
|
||||
NotConnected,
|
||||
|
||||
/// Client is already connected
|
||||
#[error("Client is already connected")]
|
||||
AlreadyConnected,
|
||||
|
||||
/// Timeout error
|
||||
#[error("Operation timed out")]
|
||||
Timeout,
|
||||
|
||||
/// Send error
|
||||
#[error("Failed to send message: {0}")]
|
||||
Send(String),
|
||||
|
||||
/// Receive error
|
||||
#[error("Failed to receive message: {0}")]
|
||||
Receive(String),
|
||||
|
||||
/// Base64 encoding/decoding error
|
||||
#[error("Base64 error: {0}")]
|
||||
Base64(String),
|
||||
|
||||
/// Hex encoding/decoding error
|
||||
#[error("Hex error: {0}")]
|
||||
Hex(String),
|
||||
|
||||
/// Generic error
|
||||
#[error("Error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// WASM version of error types (no thiserror dependency)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub enum SigSocketError {
|
||||
/// WebSocket connection error
|
||||
Connection(String),
|
||||
/// WebSocket protocol error
|
||||
Protocol(String),
|
||||
/// Message serialization/deserialization error
|
||||
Serialization(String),
|
||||
/// Invalid public key format
|
||||
InvalidPublicKey(String),
|
||||
/// Invalid URL format
|
||||
InvalidUrl(String),
|
||||
/// Client is not connected
|
||||
NotConnected,
|
||||
/// Client is already connected
|
||||
AlreadyConnected,
|
||||
/// Timeout error
|
||||
Timeout,
|
||||
/// Send error
|
||||
Send(String),
|
||||
/// Receive error
|
||||
Receive(String),
|
||||
/// Base64 encoding/decoding error
|
||||
Base64(String),
|
||||
/// Hex encoding/decoding error
|
||||
Hex(String),
|
||||
/// Generic error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl fmt::Display for SigSocketError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SigSocketError::Connection(msg) => write!(f, "Connection error: {}", msg),
|
||||
SigSocketError::Protocol(msg) => write!(f, "Protocol error: {}", msg),
|
||||
SigSocketError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||
SigSocketError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {}", msg),
|
||||
SigSocketError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
|
||||
SigSocketError::NotConnected => write!(f, "Client is not connected"),
|
||||
SigSocketError::AlreadyConnected => write!(f, "Client is already connected"),
|
||||
SigSocketError::Timeout => write!(f, "Operation timed out"),
|
||||
SigSocketError::Send(msg) => write!(f, "Failed to send message: {}", msg),
|
||||
SigSocketError::Receive(msg) => write!(f, "Failed to receive message: {}", msg),
|
||||
SigSocketError::Base64(msg) => write!(f, "Base64 error: {}", msg),
|
||||
SigSocketError::Hex(msg) => write!(f, "Hex error: {}", msg),
|
||||
SigSocketError::Other(msg) => write!(f, "Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement From traits for common error types
|
||||
impl From<serde_json::Error> for SigSocketError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
SigSocketError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for SigSocketError {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
SigSocketError::Base64(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for SigSocketError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
SigSocketError::Hex(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for SigSocketError {
|
||||
fn from(err: url::ParseError) -> Self {
|
||||
SigSocketError::InvalidUrl(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Native-specific error conversions
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native_errors {
|
||||
use super::SigSocketError;
|
||||
|
||||
impl From<tokio_tungstenite::tungstenite::Error> for SigSocketError {
|
||||
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
|
||||
SigSocketError::Connection(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WASM-specific error conversions
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl From<wasm_bindgen::JsValue> for SigSocketError {
|
||||
fn from(err: wasm_bindgen::JsValue) -> Self {
|
||||
SigSocketError::Other(format!("{:?}", err))
|
||||
}
|
||||
}
|
69
sigsocket_client/src/lib.rs
Normal file
69
sigsocket_client/src/lib.rs
Normal file
@ -0,0 +1,69 @@
|
||||
//! # SigSocket Client
|
||||
//!
|
||||
//! A WebSocket client library for connecting to sigsocket servers with WASM-first support.
|
||||
//!
|
||||
//! This library provides a unified interface for both native and WASM environments,
|
||||
//! allowing applications to connect to sigsocket servers using a public key and handle
|
||||
//! incoming signature requests.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **WASM-first design**: Optimized for browser environments
|
||||
//! - **Native support**: Works in native Rust applications
|
||||
//! - **No signing logic**: Delegates signing to the application
|
||||
//! - **User approval flow**: Notifies applications about incoming requests
|
||||
//! - **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result};
|
||||
//!
|
||||
//! struct MyHandler;
|
||||
//! impl SignRequestHandler for MyHandler {
|
||||
//! fn handle_sign_request(&self, _request: &SignRequest) -> Result<Vec<u8>> {
|
||||
//! Ok(b"fake_signature".to_vec())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<()> {
|
||||
//! // Create client with public key
|
||||
//! let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||
//! let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
//!
|
||||
//! // Set up request handler
|
||||
//! client.set_sign_handler(MyHandler);
|
||||
//!
|
||||
//! // Connect to server
|
||||
//! client.connect().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(target_arch = "wasm32", no_std)]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
mod error;
|
||||
mod protocol;
|
||||
mod client;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
|
||||
pub use error::{SigSocketError, Result};
|
||||
pub use protocol::{SignRequest, SignResponse};
|
||||
pub use client::{SigSocketClient, SignRequestHandler, ConnectionState};
|
||||
|
||||
// Re-export for convenience
|
||||
pub mod prelude {
|
||||
pub use crate::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, ConnectionState, SigSocketError, Result};
|
||||
}
|
232
sigsocket_client/src/native.rs
Normal file
232
sigsocket_client/src/native.rs
Normal file
@ -0,0 +1,232 @@
|
||||
//! Native (non-WASM) implementation of the sigsocket client
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use url::Url;
|
||||
|
||||
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// Native WebSocket client implementation
|
||||
pub struct NativeClient {
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Arc<dyn SignRequestHandler>>,
|
||||
sender: Option<mpsc::UnboundedSender<Message>>,
|
||||
connected: Arc<RwLock<bool>>,
|
||||
reconnect_attempts: u32,
|
||||
max_reconnect_attempts: u32,
|
||||
reconnect_delay_ms: u64,
|
||||
}
|
||||
|
||||
impl NativeClient {
|
||||
/// Create a new native client
|
||||
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
public_key: public_key.to_vec(),
|
||||
sign_handler: None,
|
||||
sender: None,
|
||||
connected: Arc::new(RwLock::new(false)),
|
||||
reconnect_attempts: 0,
|
||||
max_reconnect_attempts: 5,
|
||||
reconnect_delay_ms: 1000, // Start with 1 second
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler
|
||||
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||
where
|
||||
H: SignRequestHandler + 'static,
|
||||
{
|
||||
self.sign_handler = Some(Arc::new(handler));
|
||||
}
|
||||
|
||||
/// Set the sign request handler from a boxed trait object
|
||||
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||
self.sign_handler = Some(Arc::from(handler));
|
||||
}
|
||||
|
||||
/// Connect to the WebSocket server with automatic reconnection
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
self.reconnect_attempts = 0;
|
||||
self.connect_with_retry().await
|
||||
}
|
||||
|
||||
/// Connect with retry logic
|
||||
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||
loop {
|
||||
match self.try_connect().await {
|
||||
Ok(()) => {
|
||||
self.reconnect_attempts = 0; // Reset on successful connection
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
self.reconnect_attempts += 1;
|
||||
|
||||
if self.reconnect_attempts > self.max_reconnect_attempts {
|
||||
log::error!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let delay = self.reconnect_delay_ms * (2_u64.pow(self.reconnect_attempts - 1)); // Exponential backoff
|
||||
log::warn!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||
self.reconnect_attempts, self.max_reconnect_attempts, delay, e);
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Single connection attempt
|
||||
async fn try_connect(&mut self) -> Result<()> {
|
||||
let url = Url::parse(&self.url)?;
|
||||
|
||||
// Connect to WebSocket
|
||||
let (ws_stream, _) = connect_async(url).await
|
||||
.map_err(|e| SigSocketError::Connection(e.to_string()))?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Send introduction message (hex-encoded public key)
|
||||
let intro_message = hex::encode(&self.public_key);
|
||||
write.send(Message::Text(intro_message)).await
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
|
||||
// Set up message sender channel
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
self.sender = Some(tx);
|
||||
|
||||
// Set connected state
|
||||
*self.connected.write().await = true;
|
||||
|
||||
// Spawn write task
|
||||
let write_task = tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
if let Err(e) = write.send(message).await {
|
||||
log::error!("Failed to send message: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn read task
|
||||
let connected = self.connected.clone();
|
||||
let sign_handler = self.sign_handler.clone();
|
||||
let sender = self.sender.as_ref().unwrap().clone();
|
||||
|
||||
let read_task = tokio::spawn(async move {
|
||||
while let Some(message) = read.next().await {
|
||||
match message {
|
||||
Ok(Message::Text(text)) => {
|
||||
if let Err(e) = Self::handle_text_message(&text, &sign_handler, &sender).await {
|
||||
log::error!("Failed to handle message: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
log::info!("WebSocket connection closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Ignore other message types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as disconnected
|
||||
*connected.write().await = false;
|
||||
});
|
||||
|
||||
// Store tasks (in a real implementation, you'd want to manage these properly)
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::try_join!(write_task, read_task);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle incoming text messages
|
||||
async fn handle_text_message(
|
||||
text: &str,
|
||||
sign_handler: &Option<Arc<dyn SignRequestHandler>>,
|
||||
sender: &mpsc::UnboundedSender<Message>,
|
||||
) -> Result<()> {
|
||||
log::debug!("Received message: {}", text);
|
||||
|
||||
// Handle simple acknowledgment messages
|
||||
if text == "Connected" {
|
||||
log::info!("Server acknowledged connection");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to parse as sign request
|
||||
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||
if let Some(handler) = sign_handler {
|
||||
// Handle the sign request
|
||||
match handler.handle_sign_request(&sign_request) {
|
||||
Ok(signature) => {
|
||||
// Create and send response
|
||||
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||
let response_json = serde_json::to_string(&response)?;
|
||||
|
||||
sender.send(Message::Text(response_json))
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
|
||||
log::info!("Sent signature response for request {}", response.id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Sign request rejected: {}", e);
|
||||
// Optionally send an error response to the server
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No sign request handler registered, ignoring request");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::warn!("Failed to parse message: {}", text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect from the WebSocket server
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
*self.connected.write().await = false;
|
||||
|
||||
if let Some(sender) = &self.sender {
|
||||
// Send close message
|
||||
let _ = sender.send(Message::Close(None));
|
||||
}
|
||||
|
||||
self.sender = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if let Some(sender) = &self.sender {
|
||||
let response_json = serde_json::to_string(response)?;
|
||||
sender.send(Message::Text(response_json))
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if connected
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
*self.connected.read().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeClient {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup will be handled by the async tasks
|
||||
}
|
||||
}
|
141
sigsocket_client/src/protocol.rs
Normal file
141
sigsocket_client/src/protocol.rs
Normal file
@ -0,0 +1,141 @@
|
||||
//! Protocol definitions for sigsocket communication
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Sign request from the sigsocket server
|
||||
///
|
||||
/// This represents a request from the server for the client to sign a message.
|
||||
/// The client should present this to the user for approval before signing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SignRequest {
|
||||
/// Unique identifier for this request
|
||||
pub id: String,
|
||||
/// Message to be signed (base64-encoded)
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Sign response to send back to the sigsocket server
|
||||
///
|
||||
/// This represents the client's response after the user has approved and signed the message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SignResponse {
|
||||
/// Request identifier (must match the original request)
|
||||
pub id: String,
|
||||
/// Original message that was signed (base64-encoded)
|
||||
pub message: String,
|
||||
/// Signature of the message (base64-encoded)
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
impl SignRequest {
|
||||
/// Create a new sign request
|
||||
pub fn new(id: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the message as bytes (decoded from base64)
|
||||
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.message)
|
||||
}
|
||||
|
||||
/// Get the message as a hex string (for display purposes)
|
||||
pub fn message_hex(&self) -> Result<String, base64::DecodeError> {
|
||||
self.message_bytes().map(|bytes| hex::encode(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl SignResponse {
|
||||
/// Create a new sign response
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
message: impl Into<String>,
|
||||
signature: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
signature: signature.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sign response from a request and signature bytes
|
||||
pub fn from_request_and_signature(
|
||||
request: &SignRequest,
|
||||
signature: &[u8],
|
||||
) -> Self {
|
||||
Self {
|
||||
id: request.id.clone(),
|
||||
message: request.message.clone(),
|
||||
signature: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the signature as bytes (decoded from base64)
|
||||
pub fn signature_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_creation() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
assert_eq!(request.id, "test-id");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_bytes() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_hex() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_creation() {
|
||||
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl"); // "signature" in base64
|
||||
assert_eq!(response.id, "test-id");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_from_request() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"signature";
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
}
|
519
sigsocket_client/src/wasm.rs
Normal file
519
sigsocket_client/src/wasm.rs
Normal file
@ -0,0 +1,519 @@
|
||||
//! WASM implementation of the sigsocket client
|
||||
|
||||
use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, rc::Rc, format};
|
||||
use core::cell::RefCell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{WebSocket, MessageEvent, Event, BinaryType};
|
||||
|
||||
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// WASM WebSocket client implementation
|
||||
pub struct WasmClient {
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
websocket: Option<WebSocket>,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
reconnect_attempts: Rc<RefCell<u32>>,
|
||||
max_reconnect_attempts: u32,
|
||||
reconnect_delay_ms: u64,
|
||||
auto_reconnect: bool,
|
||||
}
|
||||
|
||||
impl WasmClient {
|
||||
/// Create a new WASM client
|
||||
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
public_key: public_key.to_vec(),
|
||||
sign_handler: None,
|
||||
websocket: None,
|
||||
connected: Rc::new(RefCell::new(false)),
|
||||
reconnect_attempts: Rc::new(RefCell::new(0)),
|
||||
max_reconnect_attempts: 5,
|
||||
reconnect_delay_ms: 1000, // Start with 1 second
|
||||
auto_reconnect: true, // Enable auto-reconnect by default
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler from a boxed trait object
|
||||
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||
self.sign_handler = Some(Rc::new(RefCell::new(handler)));
|
||||
}
|
||||
|
||||
/// Enable or disable automatic reconnection
|
||||
pub fn set_auto_reconnect(&mut self, enabled: bool) {
|
||||
self.auto_reconnect = enabled;
|
||||
}
|
||||
|
||||
/// Set reconnection parameters
|
||||
pub fn set_reconnect_config(&mut self, max_attempts: u32, initial_delay_ms: u64) {
|
||||
self.max_reconnect_attempts = max_attempts;
|
||||
self.reconnect_delay_ms = initial_delay_ms;
|
||||
}
|
||||
|
||||
/// Connect to the WebSocket server with automatic reconnection
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
*self.reconnect_attempts.borrow_mut() = 0;
|
||||
self.connect_with_retry().await
|
||||
}
|
||||
|
||||
/// Connect with retry logic
|
||||
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||
loop {
|
||||
match self.try_connect().await {
|
||||
Ok(()) => {
|
||||
*self.reconnect_attempts.borrow_mut() = 0; // Reset on successful connection
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
let mut attempts = self.reconnect_attempts.borrow_mut();
|
||||
*attempts += 1;
|
||||
|
||||
if *attempts > self.max_reconnect_attempts {
|
||||
web_sys::console::error_1(&format!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts).into());
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let delay = self.reconnect_delay_ms * (2_u64.pow(*attempts - 1)); // Exponential backoff
|
||||
web_sys::console::warn_1(&format!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||
*attempts, self.max_reconnect_attempts, delay, e).into());
|
||||
|
||||
// Drop the borrow before the async sleep
|
||||
drop(attempts);
|
||||
|
||||
// Wait before retrying
|
||||
self.sleep_ms(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleep for the specified number of milliseconds (WASM-compatible)
|
||||
async fn sleep_ms(&self, ms: u64) -> () {
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use js_sys::Promise;
|
||||
|
||||
let promise = Promise::new(&mut |resolve, _reject| {
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
ms as i32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
});
|
||||
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
/// Single connection attempt
|
||||
async fn try_connect(&mut self) -> Result<()> {
|
||||
// Create WebSocket
|
||||
let ws = WebSocket::new(&self.url)
|
||||
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||
|
||||
// Set binary type
|
||||
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
let connected = self.connected.clone();
|
||||
let public_key = self.public_key.clone();
|
||||
|
||||
// Set up onopen handler
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let connected = connected.clone();
|
||||
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
*connected.borrow_mut() = true;
|
||||
|
||||
// Send introduction message (hex-encoded public key)
|
||||
let intro_message = hex::encode(&public_key);
|
||||
if let Err(e) = ws_clone.send_with_str(&intro_message) {
|
||||
web_sys::console::error_1(&format!("Failed to send introduction: {:?}", e).into());
|
||||
}
|
||||
|
||||
web_sys::console::log_1(&"Connected to sigsocket server".into());
|
||||
});
|
||||
|
||||
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||
onopen_callback.forget(); // Prevent cleanup
|
||||
}
|
||||
|
||||
// Set up onmessage handler
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let handler_clone = self.sign_handler.clone();
|
||||
|
||||
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||
let message = text.as_string().unwrap_or_default();
|
||||
|
||||
// Handle the message with proper sign request support
|
||||
Self::handle_message(&message, &ws_clone, &handler_clone);
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
onmessage_callback.forget(); // Prevent cleanup
|
||||
}
|
||||
|
||||
// Set up onerror handler
|
||||
{
|
||||
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||
web_sys::console::error_1(&format!("WebSocket error: {:?}", event).into());
|
||||
});
|
||||
|
||||
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||
onerror_callback.forget(); // Prevent cleanup
|
||||
}
|
||||
|
||||
// Set up onclose handler with auto-reconnection support
|
||||
{
|
||||
let connected = connected.clone();
|
||||
let auto_reconnect = self.auto_reconnect;
|
||||
let reconnect_attempts = self.reconnect_attempts.clone();
|
||||
let max_attempts = self.max_reconnect_attempts;
|
||||
let url = self.url.clone();
|
||||
let public_key = self.public_key.clone();
|
||||
let sign_handler = self.sign_handler.clone();
|
||||
let delay_ms = self.reconnect_delay_ms;
|
||||
|
||||
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
*connected.borrow_mut() = false;
|
||||
web_sys::console::log_1(&"WebSocket connection closed".into());
|
||||
|
||||
// Trigger auto-reconnection if enabled
|
||||
if auto_reconnect {
|
||||
let attempts = reconnect_attempts.clone();
|
||||
let current_attempts = *attempts.borrow();
|
||||
|
||||
if current_attempts < max_attempts {
|
||||
web_sys::console::log_1(&"Attempting automatic reconnection...".into());
|
||||
|
||||
// Schedule reconnection attempt
|
||||
Self::schedule_reconnection(
|
||||
url.clone(),
|
||||
public_key.clone(),
|
||||
sign_handler.clone(),
|
||||
attempts.clone(),
|
||||
max_attempts,
|
||||
delay_ms,
|
||||
connected.clone(),
|
||||
);
|
||||
} else {
|
||||
web_sys::console::error_1(&format!("Max reconnection attempts ({}) reached, giving up", max_attempts).into());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||
onclose_callback.forget(); // Prevent cleanup
|
||||
}
|
||||
|
||||
self.websocket = Some(ws);
|
||||
|
||||
// Wait for connection to be established
|
||||
self.wait_for_connection().await
|
||||
}
|
||||
|
||||
/// Wait for WebSocket connection to be established
|
||||
async fn wait_for_connection(&self) -> Result<()> {
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use js_sys::Promise;
|
||||
|
||||
// Create a promise that resolves when connected or rejects on timeout
|
||||
let promise = Promise::new(&mut |resolve, reject| {
|
||||
let connected = self.connected.clone();
|
||||
let timeout_ms = 5000; // 5 second timeout
|
||||
|
||||
// Check connection status periodically
|
||||
let check_connection = Rc::new(RefCell::new(None));
|
||||
let check_connection_clone = check_connection.clone();
|
||||
|
||||
let interval_callback = Closure::wrap(Box::new(move || {
|
||||
if *connected.borrow() {
|
||||
// Connected successfully
|
||||
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||
|
||||
// Clear the interval
|
||||
if let Some(interval_id) = check_connection_clone.borrow_mut().take() {
|
||||
web_sys::window().unwrap().clear_interval_with_handle(interval_id);
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
// Set up interval to check connection every 100ms
|
||||
let interval_id = web_sys::window()
|
||||
.unwrap()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
interval_callback.as_ref().unchecked_ref(),
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
*check_connection.borrow_mut() = Some(interval_id);
|
||||
interval_callback.forget();
|
||||
|
||||
// Set up timeout
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
reject.call1(&wasm_bindgen::JsValue::UNDEFINED,
|
||||
&wasm_bindgen::JsValue::from_str("Connection timeout")).unwrap();
|
||||
|
||||
// Clear the interval on timeout
|
||||
if let Some(interval_id) = check_connection.borrow_mut().take() {
|
||||
web_sys::window().unwrap().clear_interval_with_handle(interval_id);
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
timeout_ms,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
});
|
||||
|
||||
// Wait for the promise to resolve
|
||||
JsFuture::from(promise).await
|
||||
.map_err(|_| SigSocketError::Connection("Connection timeout".to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schedule a reconnection attempt (called from onclose handler)
|
||||
fn schedule_reconnection(
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
reconnect_attempts: Rc<RefCell<u32>>,
|
||||
_max_attempts: u32,
|
||||
delay_ms: u64,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
) {
|
||||
let mut attempts = reconnect_attempts.borrow_mut();
|
||||
*attempts += 1;
|
||||
let current_attempt = *attempts;
|
||||
drop(attempts); // Release the borrow
|
||||
|
||||
let delay = delay_ms * (2_u64.pow(current_attempt - 1)); // Exponential backoff
|
||||
|
||||
web_sys::console::log_1(&format!("Scheduling reconnection attempt {} in {}ms", current_attempt, delay).into());
|
||||
|
||||
// Schedule the reconnection attempt
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
// Create a new client instance for reconnection
|
||||
match Self::attempt_reconnection(url.clone(), public_key.clone(), sign_handler.clone(), connected.clone()) {
|
||||
Ok(_) => {
|
||||
web_sys::console::log_1(&"Reconnection attempt initiated".into());
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to initiate reconnection: {:?}", e).into());
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
delay as i32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
}
|
||||
|
||||
/// Attempt to reconnect (helper method)
|
||||
fn attempt_reconnection(
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
) -> Result<()> {
|
||||
// Create WebSocket
|
||||
let ws = WebSocket::new(&url)
|
||||
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||
|
||||
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
// Send public key on open
|
||||
{
|
||||
let public_key_clone = public_key.clone();
|
||||
let connected_clone = connected.clone();
|
||||
let ws_clone = ws.clone();
|
||||
|
||||
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
web_sys::console::log_1(&"Reconnection successful - WebSocket opened".into());
|
||||
|
||||
// Send public key introduction
|
||||
let public_key_hex = hex::encode(&public_key_clone);
|
||||
if let Err(e) = ws_clone.send_with_str(&public_key_hex) {
|
||||
web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into());
|
||||
} else {
|
||||
*connected_clone.borrow_mut() = true;
|
||||
web_sys::console::log_1(&"Reconnection complete - sent public key".into());
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||
onopen_callback.forget();
|
||||
}
|
||||
|
||||
// Set up message handler for reconnected socket
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let handler_clone = sign_handler.clone();
|
||||
|
||||
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||
let message = text.as_string().unwrap_or_default();
|
||||
Self::handle_message(&message, &ws_clone, &handler_clone);
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
onmessage_callback.forget();
|
||||
}
|
||||
|
||||
// Set up error handler
|
||||
{
|
||||
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||
web_sys::console::error_1(&format!("Reconnection WebSocket error: {:?}", event).into());
|
||||
});
|
||||
|
||||
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||
onerror_callback.forget();
|
||||
}
|
||||
|
||||
// Set up close handler (for potential future reconnections)
|
||||
{
|
||||
let connected_clone = connected.clone();
|
||||
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
*connected_clone.borrow_mut() = false;
|
||||
web_sys::console::log_1(&"Reconnected WebSocket closed".into());
|
||||
});
|
||||
|
||||
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||
onclose_callback.forget();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle incoming messages with full sign request support
|
||||
fn handle_message(
|
||||
text: &str,
|
||||
ws: &WebSocket,
|
||||
sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>
|
||||
) {
|
||||
web_sys::console::log_1(&format!("Received message: {}", text).into());
|
||||
|
||||
// Handle simple acknowledgment messages
|
||||
if text == "Connected" {
|
||||
web_sys::console::log_1(&"Server acknowledged connection".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse as sign request
|
||||
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||
web_sys::console::log_1(&format!("Received sign request: {}", sign_request.id).into());
|
||||
|
||||
// Handle the sign request if we have a handler
|
||||
if let Some(handler_rc) = sign_handler {
|
||||
match handler_rc.try_borrow() {
|
||||
Ok(handler) => {
|
||||
match handler.handle_sign_request(&sign_request) {
|
||||
Ok(signature) => {
|
||||
// Create and send response
|
||||
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||
match serde_json::to_string(&response) {
|
||||
Ok(response_json) => {
|
||||
if let Err(e) = ws.send_with_str(&response_json) {
|
||||
web_sys::console::error_1(&format!("Failed to send response: {:?}", e).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&format!("Sent signature response for request {}", response.id).into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to serialize response: {}", e).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::warn_1(&format!("Sign request rejected: {}", e).into());
|
||||
// Optionally send an error response to the server
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
web_sys::console::error_1(&"Failed to borrow sign handler".into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
web_sys::console::warn_1(&"No sign request handler registered, ignoring request".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
web_sys::console::warn_1(&format!("Failed to parse message: {}", text).into());
|
||||
}
|
||||
|
||||
/// Disconnect from the WebSocket server
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(ws) = &self.websocket {
|
||||
ws.close()
|
||||
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
*self.connected.borrow_mut() = false;
|
||||
self.websocket = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if let Some(ws) = &self.websocket {
|
||||
let response_json = serde_json::to_string(response)?;
|
||||
ws.send_with_str(&response_json)
|
||||
.map_err(|e| SigSocketError::Send(format!("{:?}", e)))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if connected
|
||||
pub fn is_connected(&self) -> bool {
|
||||
*self.connected.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WasmClient {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup will be handled by the WebSocket close
|
||||
}
|
||||
}
|
||||
|
||||
// WASM-specific utilities
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
// Helper macro for logging in WASM
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||
}
|
162
sigsocket_client/tests/integration_test.rs
Normal file
162
sigsocket_client/tests/integration_test.rs
Normal file
@ -0,0 +1,162 @@
|
||||
//! Integration tests for sigsocket_client
|
||||
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// Test sign request handler
|
||||
struct TestSignHandler {
|
||||
should_approve: bool,
|
||||
}
|
||||
|
||||
impl TestSignHandler {
|
||||
fn new(should_approve: bool) -> Self {
|
||||
Self { should_approve }
|
||||
}
|
||||
}
|
||||
|
||||
impl SignRequestHandler for TestSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
if self.should_approve {
|
||||
// Create a test signature
|
||||
let signature = format!("test_signature_for_{}", request.id);
|
||||
Ok(signature.into_bytes())
|
||||
} else {
|
||||
Err(SigSocketError::Other("User rejected request".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_creation() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(request.id, "test-123");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_decoding() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_creation() {
|
||||
let response = SignResponse::new("test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
assert_eq!(response.id, "test-123");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_from_request() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"test_signature";
|
||||
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_serialization() {
|
||||
// Test SignRequest serialization
|
||||
let request = SignRequest::new("req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
// Test SignResponse serialization
|
||||
let response = SignResponse::new("req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_creation() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||
assert!(!client.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_invalid_url() {
|
||||
let public_key = vec![1, 2, 3];
|
||||
let result = SigSocketClient::new("invalid-url", public_key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_empty_public_key() {
|
||||
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||
assert!(result.is_err());
|
||||
if let Err(error) = result {
|
||||
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_handler_approval() {
|
||||
let handler = TestSignHandler::new(true);
|
||||
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let signature = result.unwrap();
|
||||
assert_eq!(signature, b"test_signature_for_test-789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_handler_rejection() {
|
||||
let handler = TestSignHandler::new(false);
|
||||
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_display() {
|
||||
let error = SigSocketError::NotConnected;
|
||||
assert_eq!(error.to_string(), "Client is not connected");
|
||||
|
||||
let error = SigSocketError::Connection("test error".to_string());
|
||||
assert_eq!(error.to_string(), "Connection error: test error");
|
||||
}
|
||||
|
||||
// Test that demonstrates the expected usage pattern
|
||||
#[test]
|
||||
fn test_usage_pattern() {
|
||||
// 1. Create client
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// 2. Set handler
|
||||
client.set_sign_handler(TestSignHandler::new(true));
|
||||
|
||||
// 3. Verify state
|
||||
assert!(!client.is_connected());
|
||||
|
||||
// 4. Create a test request/response cycle
|
||||
let request = SignRequest::new("test-request", "dGVzdCBtZXNzYWdl");
|
||||
let handler = TestSignHandler::new(true);
|
||||
let signature = handler.handle_sign_request(&request).unwrap();
|
||||
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||
|
||||
// 5. Verify the response
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
181
sigsocket_client/tests/wasm_tests.rs
Normal file
181
sigsocket_client/tests/wasm_tests.rs
Normal file
@ -0,0 +1,181 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
//! WASM/browser tests for sigsocket_client using wasm-bindgen-test
|
||||
|
||||
use wasm_bindgen_test::*;
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
/// Test sign request handler for WASM tests
|
||||
struct TestWasmSignHandler {
|
||||
should_approve: bool,
|
||||
}
|
||||
|
||||
impl TestWasmSignHandler {
|
||||
fn new(should_approve: bool) -> Self {
|
||||
Self { should_approve }
|
||||
}
|
||||
}
|
||||
|
||||
impl SignRequestHandler for TestWasmSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
if self.should_approve {
|
||||
// Create a test signature
|
||||
let signature = format!("wasm_test_signature_for_{}", request.id);
|
||||
Ok(signature.into_bytes())
|
||||
} else {
|
||||
Err(SigSocketError::Other("User rejected request in WASM test".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_request_creation_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(request.id, "wasm-test-123");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_request_message_decoding_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_response_creation_wasm() {
|
||||
let response = SignResponse::new("wasm-test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
assert_eq!(response.id, "wasm-test-123");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_response_from_request_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"wasm_test_signature";
|
||||
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_protocol_serialization_wasm() {
|
||||
// Test SignRequest serialization
|
||||
let request = SignRequest::new("wasm-req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
// Test SignResponse serialization
|
||||
let response = SignResponse::new("wasm-req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_creation_wasm() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||
assert!(!client.is_connected());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_invalid_url_wasm() {
|
||||
let public_key = vec![1, 2, 3];
|
||||
let result = SigSocketClient::new("invalid-url", public_key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_empty_public_key_wasm() {
|
||||
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||
assert!(result.is_err());
|
||||
if let Err(error) = result {
|
||||
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_handler_approval_wasm() {
|
||||
let handler = TestWasmSignHandler::new(true);
|
||||
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let signature = result.unwrap();
|
||||
assert_eq!(signature, b"wasm_test_signature_for_wasm-test-789");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_handler_rejection_wasm() {
|
||||
let handler = TestWasmSignHandler::new(false);
|
||||
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_error_display_wasm() {
|
||||
let error = SigSocketError::NotConnected;
|
||||
assert_eq!(error.to_string(), "Client is not connected");
|
||||
|
||||
let error = SigSocketError::Connection("wasm test error".to_string());
|
||||
assert_eq!(error.to_string(), "Connection error: wasm test error");
|
||||
}
|
||||
|
||||
// Test that demonstrates the expected WASM usage pattern
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wasm_usage_pattern() {
|
||||
// 1. Create client
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// 2. Set handler
|
||||
client.set_sign_handler(TestWasmSignHandler::new(true));
|
||||
|
||||
// 3. Verify state
|
||||
assert!(!client.is_connected());
|
||||
|
||||
// 4. Create a test request/response cycle
|
||||
let request = SignRequest::new("wasm-test-request", "dGVzdCBtZXNzYWdl");
|
||||
let handler = TestWasmSignHandler::new(true);
|
||||
let signature = handler.handle_sign_request(&request).unwrap();
|
||||
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||
|
||||
// 5. Verify the response
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
// Test WASM-specific console logging (if needed)
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wasm_console_logging() {
|
||||
// This test verifies that WASM console logging works
|
||||
web_sys::console::log_1(&"SigSocket WASM test logging works!".into());
|
||||
|
||||
// Test that we can create and log protocol messages
|
||||
let request = SignRequest::new("log-test", "dGVzdA==");
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
web_sys::console::log_1(&format!("Sign request JSON: {}", json).into());
|
||||
|
||||
// This test always passes - it's just for verification that logging works
|
||||
assert!(true);
|
||||
}
|
Loading…
Reference in New Issue
Block a user