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:
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()))
|
||||
}
|
Reference in New Issue
Block a user