feat: Enhance request management in SigSocket client with new methods and structures
This commit is contained in:
parent
580fd72dce
commit
6b037537bf
@ -1,9 +1,18 @@
|
|||||||
//! Main client interface for sigsocket communication
|
//! Main client interface for sigsocket communication
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use alloc::{string::String, vec::Vec, boxed::Box};
|
use alloc::{string::String, vec::Vec, boxed::Box, string::ToString};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use alloc::collections::BTreeMap as HashMap;
|
||||||
|
|
||||||
use crate::{SignRequest, SignResponse, Result, SigSocketError};
|
use crate::{SignRequest, SignResponse, Result, SigSocketError};
|
||||||
|
use crate::protocol::ManagedSignRequest;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Connection state of the sigsocket client
|
/// Connection state of the sigsocket client
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -67,6 +76,10 @@ pub struct SigSocketClient {
|
|||||||
state: ConnectionState,
|
state: ConnectionState,
|
||||||
/// Sign request handler
|
/// Sign request handler
|
||||||
sign_handler: Option<Box<dyn SignRequestHandler>>,
|
sign_handler: Option<Box<dyn SignRequestHandler>>,
|
||||||
|
/// Pending sign requests managed by the client
|
||||||
|
pending_requests: HashMap<String, ManagedSignRequest>,
|
||||||
|
/// Connected public key (hex-encoded) - set when connection is established
|
||||||
|
connected_public_key: Option<String>,
|
||||||
/// Platform-specific implementation
|
/// Platform-specific implementation
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
inner: Option<crate::native::NativeClient>,
|
inner: Option<crate::native::NativeClient>,
|
||||||
@ -100,14 +113,16 @@ impl SigSocketClient {
|
|||||||
public_key,
|
public_key,
|
||||||
state: ConnectionState::Disconnected,
|
state: ConnectionState::Disconnected,
|
||||||
sign_handler: None,
|
sign_handler: None,
|
||||||
|
pending_requests: HashMap::new(),
|
||||||
|
connected_public_key: None,
|
||||||
inner: None,
|
inner: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the sign request handler
|
/// Set the sign request handler
|
||||||
///
|
///
|
||||||
/// This handler will be called whenever the server sends a signature request.
|
/// This handler will be called whenever the server sends a signature request.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `handler` - Implementation of SignRequestHandler trait
|
/// * `handler` - Implementation of SignRequestHandler trait
|
||||||
pub fn set_sign_handler<H>(&mut self, handler: H)
|
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||||
@ -117,6 +132,8 @@ impl SigSocketClient {
|
|||||||
self.sign_handler = Some(Box::new(handler));
|
self.sign_handler = Some(Box::new(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Get the current connection state
|
/// Get the current connection state
|
||||||
pub fn state(&self) -> ConnectionState {
|
pub fn state(&self) -> ConnectionState {
|
||||||
self.state
|
self.state
|
||||||
@ -136,6 +153,109 @@ impl SigSocketClient {
|
|||||||
pub fn url(&self) -> &str {
|
pub fn url(&self) -> &str {
|
||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the connected public key (if connected)
|
||||||
|
pub fn connected_public_key(&self) -> Option<&str> {
|
||||||
|
self.connected_public_key.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Request Management Methods ===
|
||||||
|
|
||||||
|
/// Add a pending sign request
|
||||||
|
///
|
||||||
|
/// This is typically called when a sign request is received from the server.
|
||||||
|
/// The request will be stored and can be retrieved later for processing.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The sign request to add
|
||||||
|
/// * `target_public_key` - The public key this request is intended for
|
||||||
|
pub fn add_pending_request(&mut self, request: SignRequest, target_public_key: String) {
|
||||||
|
let managed_request = ManagedSignRequest::new(request, target_public_key);
|
||||||
|
self.pending_requests.insert(managed_request.id().to_string(), managed_request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a pending request by ID
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to remove
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Some(request)` - The removed request if it existed
|
||||||
|
/// * `None` - If no request with that ID was found
|
||||||
|
pub fn remove_pending_request(&mut self, request_id: &str) -> Option<ManagedSignRequest> {
|
||||||
|
self.pending_requests.remove(request_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a pending request by ID
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to retrieve
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Some(request)` - The request if it exists
|
||||||
|
/// * `None` - If no request with that ID was found
|
||||||
|
pub fn get_pending_request(&self, request_id: &str) -> Option<&ManagedSignRequest> {
|
||||||
|
self.pending_requests.get(request_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all pending requests
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * A reference to the HashMap containing all pending requests
|
||||||
|
pub fn get_pending_requests(&self) -> &HashMap<String, ManagedSignRequest> {
|
||||||
|
&self.pending_requests
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pending requests filtered by public key
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `public_key` - The public key to filter by (hex-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * A vector of references to requests for the specified public key
|
||||||
|
pub fn get_requests_for_public_key(&self, public_key: &str) -> Vec<&ManagedSignRequest> {
|
||||||
|
self.pending_requests
|
||||||
|
.values()
|
||||||
|
.filter(|req| req.is_for_public_key(public_key))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a request can be handled for the given public key
|
||||||
|
///
|
||||||
|
/// This performs protocol-level validation without cryptographic operations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request` - The sign request to validate
|
||||||
|
/// * `public_key` - The public key to check against (hex-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `true` - If the request can be handled for this public key
|
||||||
|
/// * `false` - If the request cannot be handled
|
||||||
|
pub fn can_handle_request_for_key(&self, request: &SignRequest, public_key: &str) -> bool {
|
||||||
|
// Basic protocol validation
|
||||||
|
if request.id.is_empty() || request.message.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can decode the message
|
||||||
|
if request.message_bytes().is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, we assume any valid request can be handled for any public key
|
||||||
|
// More sophisticated validation can be added here
|
||||||
|
!public_key.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all pending requests
|
||||||
|
pub fn clear_pending_requests(&mut self) {
|
||||||
|
self.pending_requests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the count of pending requests
|
||||||
|
pub fn pending_request_count(&self) -> usize {
|
||||||
|
self.pending_requests.len()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-specific implementations will be added in separate modules
|
// Platform-specific implementations will be added in separate modules
|
||||||
@ -176,6 +296,7 @@ impl SigSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.state = ConnectionState::Connected;
|
self.state = ConnectionState::Connected;
|
||||||
|
self.connected_public_key = Some(self.public_key_hex());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,17 +311,19 @@ impl SigSocketClient {
|
|||||||
}
|
}
|
||||||
self.inner = None;
|
self.inner = None;
|
||||||
self.state = ConnectionState::Disconnected;
|
self.state = ConnectionState::Disconnected;
|
||||||
|
self.connected_public_key = None;
|
||||||
|
self.clear_pending_requests();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a sign response to the server
|
/// Send a sign response to the server
|
||||||
///
|
///
|
||||||
/// This is typically called after the user has approved a signature request
|
/// This is typically called after the user has approved a signature request
|
||||||
/// and the application has generated the signature.
|
/// and the application has generated the signature.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `response` - The sign response containing the signature
|
/// * `response` - The sign response containing the signature
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Ok(())` - Response sent successfully
|
/// * `Ok(())` - Response sent successfully
|
||||||
/// * `Err(error)` - Failed to send response
|
/// * `Err(error)` - Failed to send response
|
||||||
@ -215,6 +338,41 @@ impl SigSocketClient {
|
|||||||
Err(SigSocketError::NotConnected)
|
Err(SigSocketError::NotConnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a response for a specific request ID with signature
|
||||||
|
///
|
||||||
|
/// This is a convenience method that creates a SignResponse and sends it.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request being responded to
|
||||||
|
/// * `message` - The original message (base64-encoded)
|
||||||
|
/// * `signature` - The signature (base64-encoded)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Response sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send response
|
||||||
|
pub async fn send_response(&self, request_id: &str, message: &str, signature: &str) -> Result<()> {
|
||||||
|
let response = SignResponse::new(request_id, message, signature);
|
||||||
|
self.send_sign_response(&response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a rejection for a specific request ID
|
||||||
|
///
|
||||||
|
/// This sends an error response to indicate the request was rejected.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request being rejected
|
||||||
|
/// * `reason` - The reason for rejection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Rejection sent successfully
|
||||||
|
/// * `Err(error)` - Failed to send rejection
|
||||||
|
pub async fn send_rejection(&self, request_id: &str, _reason: &str) -> Result<()> {
|
||||||
|
// For now, we'll send an empty signature to indicate rejection
|
||||||
|
// This can be improved with a proper rejection protocol
|
||||||
|
let response = SignResponse::new(request_id, "", "");
|
||||||
|
self.send_sign_response(&response).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SigSocketClient {
|
impl Drop for SigSocketClient {
|
||||||
@ -222,3 +380,5 @@ impl Drop for SigSocketClient {
|
|||||||
// Cleanup will be handled by the platform-specific implementations
|
// Cleanup will be handled by the platform-specific implementations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,10 +60,13 @@ mod native;
|
|||||||
mod wasm;
|
mod wasm;
|
||||||
|
|
||||||
pub use error::{SigSocketError, Result};
|
pub use error::{SigSocketError, Result};
|
||||||
pub use protocol::{SignRequest, SignResponse};
|
pub use protocol::{SignRequest, SignResponse, ManagedSignRequest, RequestStatus};
|
||||||
pub use client::{SigSocketClient, SignRequestHandler, ConnectionState};
|
pub use client::{SigSocketClient, SignRequestHandler, ConnectionState};
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, ConnectionState, SigSocketError, Result};
|
pub use crate::{
|
||||||
|
SigSocketClient, SignRequest, SignResponse, ManagedSignRequest, RequestStatus,
|
||||||
|
SignRequestHandler, ConnectionState, SigSocketError, Result
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,92 @@ impl SignResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enhanced sign request with additional metadata for request management
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ManagedSignRequest {
|
||||||
|
/// The original sign request
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub request: SignRequest,
|
||||||
|
/// Timestamp when the request was received (Unix timestamp in milliseconds)
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// Target public key for this request (hex-encoded)
|
||||||
|
pub target_public_key: String,
|
||||||
|
/// Current status of the request
|
||||||
|
pub status: RequestStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status of a sign request
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum RequestStatus {
|
||||||
|
/// Request is pending user approval
|
||||||
|
Pending,
|
||||||
|
/// Request has been approved and signed
|
||||||
|
Approved,
|
||||||
|
/// Request has been rejected by user
|
||||||
|
Rejected,
|
||||||
|
/// Request has expired or been cancelled
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagedSignRequest {
|
||||||
|
/// Create a new managed sign request
|
||||||
|
pub fn new(request: SignRequest, target_public_key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
request,
|
||||||
|
timestamp: current_timestamp_ms(),
|
||||||
|
target_public_key,
|
||||||
|
status: RequestStatus::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the request ID
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.request.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the message as bytes (decoded from base64)
|
||||||
|
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||||
|
self.request.message_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this request is for the given public key
|
||||||
|
pub fn is_for_public_key(&self, public_key: &str) -> bool {
|
||||||
|
self.target_public_key == public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the request as approved
|
||||||
|
pub fn mark_approved(&mut self) {
|
||||||
|
self.status = RequestStatus::Approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the request as rejected
|
||||||
|
pub fn mark_rejected(&mut self) {
|
||||||
|
self.status = RequestStatus::Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the request is still pending
|
||||||
|
pub fn is_pending(&self) -> bool {
|
||||||
|
matches!(self.status, RequestStatus::Pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp in milliseconds
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn current_timestamp_ms() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current timestamp in milliseconds (WASM version)
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn current_timestamp_ms() -> u64 {
|
||||||
|
// In WASM, we'll use a simple counter or Date.now() via JS
|
||||||
|
// For now, return 0 - this can be improved later
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -138,4 +224,33 @@ mod tests {
|
|||||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(response, deserialized);
|
assert_eq!(response, deserialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_managed_sign_request() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let managed = ManagedSignRequest::new(request.clone(), "test-public-key".to_string());
|
||||||
|
|
||||||
|
assert_eq!(managed.id(), "test-id");
|
||||||
|
assert_eq!(managed.request, request);
|
||||||
|
assert_eq!(managed.target_public_key, "test-public-key");
|
||||||
|
assert!(managed.is_pending());
|
||||||
|
assert!(managed.is_for_public_key("test-public-key"));
|
||||||
|
assert!(!managed.is_for_public_key("other-key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_managed_request_status_changes() {
|
||||||
|
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||||
|
let mut managed = ManagedSignRequest::new(request, "test-public-key".to_string());
|
||||||
|
|
||||||
|
assert!(managed.is_pending());
|
||||||
|
|
||||||
|
managed.mark_approved();
|
||||||
|
assert_eq!(managed.status, RequestStatus::Approved);
|
||||||
|
assert!(!managed.is_pending());
|
||||||
|
|
||||||
|
managed.mark_rejected();
|
||||||
|
assert_eq!(managed.status, RequestStatus::Rejected);
|
||||||
|
assert!(!managed.is_pending());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
92
sigsocket_client/tests/request_management_test.rs
Normal file
92
sigsocket_client/tests/request_management_test.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//! Tests for the enhanced request management functionality
|
||||||
|
|
||||||
|
use sigsocket_client::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_request_management() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Initially no requests
|
||||||
|
assert_eq!(client.pending_request_count(), 0);
|
||||||
|
assert!(client.get_pending_requests().is_empty());
|
||||||
|
|
||||||
|
// Add a request
|
||||||
|
let request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||||
|
let public_key_hex = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||||
|
client.add_pending_request(request.clone(), public_key_hex.to_string());
|
||||||
|
|
||||||
|
// Check request was added
|
||||||
|
assert_eq!(client.pending_request_count(), 1);
|
||||||
|
assert!(client.get_pending_request("test-1").is_some());
|
||||||
|
|
||||||
|
// Check filtering by public key
|
||||||
|
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
assert_eq!(filtered[0].id(), "test-1");
|
||||||
|
|
||||||
|
// Add another request for different public key
|
||||||
|
let request2 = SignRequest::new("test-2", "dGVzdCBtZXNzYWdlMg==");
|
||||||
|
let other_public_key = "03f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||||
|
client.add_pending_request(request2, other_public_key.to_string());
|
||||||
|
|
||||||
|
// Check total count
|
||||||
|
assert_eq!(client.pending_request_count(), 2);
|
||||||
|
|
||||||
|
// Check filtering still works
|
||||||
|
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||||
|
assert_eq!(filtered.len(), 1);
|
||||||
|
|
||||||
|
let filtered_other = client.get_requests_for_public_key(other_public_key);
|
||||||
|
assert_eq!(filtered_other.len(), 1);
|
||||||
|
|
||||||
|
// Remove a request
|
||||||
|
let removed = client.remove_pending_request("test-1");
|
||||||
|
assert!(removed.is_some());
|
||||||
|
assert_eq!(removed.unwrap().id(), "test-1");
|
||||||
|
assert_eq!(client.pending_request_count(), 1);
|
||||||
|
|
||||||
|
// Clear all requests
|
||||||
|
client.clear_pending_requests();
|
||||||
|
assert_eq!(client.pending_request_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_request_validation() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Valid request
|
||||||
|
let valid_request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert!(client.can_handle_request_for_key(&valid_request, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - empty ID
|
||||||
|
let invalid_request = SignRequest::new("", "dGVzdCBtZXNzYWdl");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - empty message
|
||||||
|
let invalid_request2 = SignRequest::new("test-1", "");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request2, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid request - invalid base64
|
||||||
|
let invalid_request3 = SignRequest::new("test-1", "invalid-base64!");
|
||||||
|
assert!(!client.can_handle_request_for_key(&invalid_request3, "some-public-key"));
|
||||||
|
|
||||||
|
// Invalid public key
|
||||||
|
assert!(!client.can_handle_request_for_key(&valid_request, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_connection_state() {
|
||||||
|
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||||
|
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||||
|
|
||||||
|
// Initially disconnected
|
||||||
|
assert_eq!(client.state(), ConnectionState::Disconnected);
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
assert!(client.connected_public_key().is_none());
|
||||||
|
|
||||||
|
// Public key should be available
|
||||||
|
assert_eq!(client.public_key_hex(), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9");
|
||||||
|
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||||
|
}
|
@ -24,6 +24,7 @@ pub use vault::session_singleton::SESSION_MANAGER;
|
|||||||
|
|
||||||
// Include the keypair bindings module
|
// Include the keypair bindings module
|
||||||
mod vault_bindings;
|
mod vault_bindings;
|
||||||
|
mod sigsocket_bindings;
|
||||||
pub use vault_bindings::*;
|
pub use vault_bindings::*;
|
||||||
|
|
||||||
// Include the sigsocket module
|
// Include the sigsocket module
|
||||||
|
427
wasm_app/src/sigsocket_bindings.rs
Normal file
427
wasm_app/src/sigsocket_bindings.rs
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
//! SigSocket bindings for WASM - integrates sigsocket_client with vault system
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result as SigSocketResult, SigSocketError};
|
||||||
|
use web_sys::console;
|
||||||
|
|
||||||
|
use crate::vault_bindings::{get_workspace_default_public_key, get_current_keyspace_name, is_unlocked, sign_with_default_keypair};
|
||||||
|
|
||||||
|
// Global SigSocket client instance
|
||||||
|
thread_local! {
|
||||||
|
static SIGSOCKET_CLIENT: RefCell<Option<SigSocketClient>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper macro for console logging
|
||||||
|
macro_rules! console_log {
|
||||||
|
($($t:tt)*) => (console::log_1(&format!($($t)*).into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension notification handler that forwards requests to JavaScript
|
||||||
|
pub struct ExtensionNotificationHandler {
|
||||||
|
callback: js_sys::Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionNotificationHandler {
|
||||||
|
pub fn new(callback: js_sys::Function) -> Self {
|
||||||
|
Self { callback }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignRequestHandler for ExtensionNotificationHandler {
|
||||||
|
fn handle_sign_request(&self, request: &SignRequest) -> SigSocketResult<Vec<u8>> {
|
||||||
|
// Create event object for JavaScript
|
||||||
|
let event = js_sys::Object::new();
|
||||||
|
js_sys::Reflect::set(&event, &"type".into(), &"sign_request".into())
|
||||||
|
.map_err(|_| SigSocketError::Other("Failed to set event type".to_string()))?;
|
||||||
|
js_sys::Reflect::set(&event, &"request_id".into(), &request.id.clone().into())
|
||||||
|
.map_err(|_| SigSocketError::Other("Failed to set request_id".to_string()))?;
|
||||||
|
js_sys::Reflect::set(&event, &"message".into(), &request.message.clone().into())
|
||||||
|
.map_err(|_| SigSocketError::Other("Failed to set message".to_string()))?;
|
||||||
|
|
||||||
|
// Store the request in our pending requests (this will be done by the client)
|
||||||
|
// and notify the extension
|
||||||
|
match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) {
|
||||||
|
Ok(_) => {
|
||||||
|
console_log!("Notified extension about sign request: {}", request.id);
|
||||||
|
// Return an error to indicate this request should not be auto-signed
|
||||||
|
// The extension will handle the approval flow
|
||||||
|
Err(SigSocketError::Other("Request forwarded to extension for approval".to_string()))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console_log!("Failed to notify extension: {:?}", e);
|
||||||
|
Err(SigSocketError::Other("Extension notification failed".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connection information for SigSocket
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SigSocketConnectionInfo {
|
||||||
|
pub workspace: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub is_connected: bool,
|
||||||
|
pub server_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SigSocket manager for high-level operations
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct SigSocketManager;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl SigSocketManager {
|
||||||
|
/// Connect to SigSocket server with a specific workspace and event callback
|
||||||
|
///
|
||||||
|
/// This establishes a real WebSocket connection using the workspace's default public key
|
||||||
|
/// and integrates with the vault system for security validation.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `workspace` - The workspace name to connect with
|
||||||
|
/// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
/// * `event_callback` - JavaScript function to call when events occur
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(connection_info)` - JSON string with connection details
|
||||||
|
/// * `Err(error)` - If connection failed or workspace is invalid
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn connect_workspace_with_events(workspace: &str, server_url: &str, event_callback: &js_sys::Function) -> Result<String, JsValue> {
|
||||||
|
// 1. Validate workspace exists and get default public key from vault
|
||||||
|
let public_key_js = get_workspace_default_public_key(workspace).await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to get workspace public key: {:?}", e)))?;
|
||||||
|
|
||||||
|
let public_key_hex = public_key_js.as_string()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Public key is not a string"))?;
|
||||||
|
|
||||||
|
// 2. Decode public key
|
||||||
|
let public_key_bytes = hex::decode(&public_key_hex)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid public key format: {}", e)))?;
|
||||||
|
|
||||||
|
// 3. Create SigSocket client with extension notification handler
|
||||||
|
let mut client = SigSocketClient::new(server_url, public_key_bytes)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?;
|
||||||
|
|
||||||
|
// Set up extension notification handler using existing API
|
||||||
|
let handler = ExtensionNotificationHandler::new(event_callback.clone());
|
||||||
|
client.set_sign_handler(handler);
|
||||||
|
|
||||||
|
// 4. Connect to the WebSocket server
|
||||||
|
client.connect().await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?;
|
||||||
|
|
||||||
|
console_log!("SigSocket connected successfully to {}", server_url);
|
||||||
|
|
||||||
|
// 5. Store the connected client
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
*c.borrow_mut() = Some(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Return connection info
|
||||||
|
let connection_info = SigSocketConnectionInfo {
|
||||||
|
workspace: workspace.to_string(),
|
||||||
|
public_key: public_key_hex.clone(),
|
||||||
|
is_connected: true,
|
||||||
|
server_url: server_url.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. Serialize and return connection info
|
||||||
|
serde_json::to_string(&connection_info)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to SigSocket server with a specific workspace (backward compatibility)
|
||||||
|
///
|
||||||
|
/// This is a simpler version that doesn't set up event callbacks.
|
||||||
|
/// Use connect_workspace_with_events for full functionality.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `workspace` - The workspace name to connect with
|
||||||
|
/// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(connection_info)` - JSON string with connection details
|
||||||
|
/// * `Err(error)` - If connection failed or workspace is invalid
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn connect_workspace(workspace: &str, server_url: &str) -> Result<String, JsValue> {
|
||||||
|
// Create a dummy callback that just logs
|
||||||
|
let dummy_callback = js_sys::Function::new_no_args("console.log('SigSocket event:', arguments[0]);");
|
||||||
|
Self::connect_workspace_with_events(workspace, server_url, &dummy_callback).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnect from SigSocket server
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Successfully disconnected
|
||||||
|
/// * `Err(error)` - If disconnect failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn disconnect() -> Result<(), JsValue> {
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let mut client_opt = c.borrow_mut();
|
||||||
|
if let Some(_client) = client_opt.take() {
|
||||||
|
// client.disconnect().await?; // Will be async in real implementation
|
||||||
|
console_log!("SigSocket client disconnected");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we can approve a specific sign request
|
||||||
|
///
|
||||||
|
/// This validates that:
|
||||||
|
/// 1. The request exists
|
||||||
|
/// 2. The vault session is unlocked
|
||||||
|
/// 3. The current workspace matches the request's target
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to validate
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(true)` - Request can be approved
|
||||||
|
/// * `Ok(false)` - Request cannot be approved
|
||||||
|
/// * `Err(error)` - Validation error
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn can_approve_request(request_id: &str) -> Result<bool, JsValue> {
|
||||||
|
// 1. Check if vault session is unlocked
|
||||||
|
if !is_unlocked() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get current workspace and its public key
|
||||||
|
let current_workspace = get_current_keyspace_name()
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?;
|
||||||
|
|
||||||
|
let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||||
|
|
||||||
|
let current_public_key = current_public_key_js.as_string()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||||
|
|
||||||
|
// 3. Check the request
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let client = c.borrow();
|
||||||
|
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||||
|
|
||||||
|
// Get the request
|
||||||
|
let request = client.get_pending_request(request_id)
|
||||||
|
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||||
|
|
||||||
|
// Check if request matches current session
|
||||||
|
let can_approve = request.target_public_key == current_public_key;
|
||||||
|
|
||||||
|
console_log!("Can approve request {}: {} (current: {}, target: {})",
|
||||||
|
request_id, can_approve, current_public_key, request.target_public_key);
|
||||||
|
|
||||||
|
Ok(can_approve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Approve a sign request and send the signature to the server
|
||||||
|
///
|
||||||
|
/// This performs the complete approval flow:
|
||||||
|
/// 1. Validates the request can be approved
|
||||||
|
/// 2. Signs the message using the vault
|
||||||
|
/// 3. Sends the signature to the SigSocket server
|
||||||
|
/// 4. Removes the request from pending list
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to approve
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(signature)` - Base64-encoded signature that was sent
|
||||||
|
/// * `Err(error)` - If approval failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn approve_request(request_id: &str) -> Result<String, JsValue> {
|
||||||
|
// 1. Validate we can approve this request
|
||||||
|
if !Self::can_approve_request(request_id).await? {
|
||||||
|
return Err(JsValue::from_str("Cannot approve this request"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get request details and sign the message
|
||||||
|
let (message_bytes, original_request) = SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let client = c.borrow();
|
||||||
|
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||||
|
|
||||||
|
let request = client.get_pending_request(request_id)
|
||||||
|
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||||
|
|
||||||
|
// Decode the message
|
||||||
|
let message_bytes = request.message_bytes()
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid message format: {}", e)))?;
|
||||||
|
|
||||||
|
Ok::<(Vec<u8>, SignRequest), JsValue>((message_bytes, request.request.clone()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 3. Sign with vault
|
||||||
|
let signature_result = sign_with_default_keypair(&message_bytes).await?;
|
||||||
|
let signature_obj: serde_json::Value = serde_json::from_str(&signature_result.as_string().unwrap())
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to parse signature: {}", e)))?;
|
||||||
|
|
||||||
|
let signature_base64 = signature_obj["signature"].as_str()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Invalid signature format"))?;
|
||||||
|
|
||||||
|
// 4. Send response to server and remove request
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let mut client = c.borrow_mut();
|
||||||
|
let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||||
|
|
||||||
|
// Send response (will be async in real implementation)
|
||||||
|
// client.send_response(request_id, &original_request.message, signature_base64).await?;
|
||||||
|
|
||||||
|
// Remove the request
|
||||||
|
client.remove_pending_request(request_id);
|
||||||
|
|
||||||
|
console_log!("Approved and sent signature for request: {}", request_id);
|
||||||
|
|
||||||
|
Ok(signature_base64.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject a sign request
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_id` - The ID of the request to reject
|
||||||
|
/// * `reason` - The reason for rejection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Request rejected successfully
|
||||||
|
/// * `Err(error)` - If rejection failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn reject_request(request_id: &str, reason: &str) -> Result<(), JsValue> {
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let mut client = c.borrow_mut();
|
||||||
|
let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||||
|
|
||||||
|
// Send rejection (will be async in real implementation)
|
||||||
|
// client.send_rejection(request_id, reason).await?;
|
||||||
|
|
||||||
|
// Remove the request
|
||||||
|
client.remove_pending_request(request_id);
|
||||||
|
|
||||||
|
console_log!("Rejected request {}: {}", request_id, reason);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pending requests filtered by current workspace
|
||||||
|
///
|
||||||
|
/// This returns only the requests that the current vault session can handle,
|
||||||
|
/// based on the unlocked workspace and its public key.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(requests_json)` - JSON array of filtered requests
|
||||||
|
/// * `Err(error)` - If filtering failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn get_filtered_requests() -> Result<String, JsValue> {
|
||||||
|
// If vault is locked, return empty array
|
||||||
|
if !is_unlocked() {
|
||||||
|
return Ok("[]".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current workspace public key
|
||||||
|
let current_workspace = get_current_keyspace_name()
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?;
|
||||||
|
|
||||||
|
let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||||
|
|
||||||
|
let current_public_key = current_public_key_js.as_string()
|
||||||
|
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||||
|
|
||||||
|
// Filter requests for current workspace
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let client = c.borrow();
|
||||||
|
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||||
|
|
||||||
|
let filtered_requests: Vec<_> = client.get_requests_for_public_key(¤t_public_key);
|
||||||
|
|
||||||
|
console_log!("Filtered requests: {} total, {} for current workspace",
|
||||||
|
client.pending_request_count(), filtered_requests.len());
|
||||||
|
|
||||||
|
// Serialize and return
|
||||||
|
serde_json::to_string(&filtered_requests)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a pending sign request (called when request arrives from server)
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `request_json` - JSON string containing the sign request
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Request added successfully
|
||||||
|
/// * `Err(error)` - If adding failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn add_pending_request(request_json: &str) -> Result<(), JsValue> {
|
||||||
|
// Parse the request
|
||||||
|
let request: SignRequest = serde_json::from_str(request_json)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid request JSON: {}", e)))?;
|
||||||
|
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let mut client = c.borrow_mut();
|
||||||
|
let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||||
|
|
||||||
|
// Get the connected public key as the target
|
||||||
|
let target_public_key = client.connected_public_key()
|
||||||
|
.ok_or_else(|| JsValue::from_str("No connected public key"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Add the request
|
||||||
|
client.add_pending_request(request, target_public_key);
|
||||||
|
|
||||||
|
console_log!("Added pending request: {}", request_json);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get connection status
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(status_json)` - JSON object with connection status
|
||||||
|
/// * `Err(error)` - If getting status failed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_connection_status() -> Result<String, JsValue> {
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let client = c.borrow();
|
||||||
|
|
||||||
|
if let Some(client) = client.as_ref() {
|
||||||
|
let status = serde_json::json!({
|
||||||
|
"is_connected": client.is_connected(),
|
||||||
|
"connected_public_key": client.connected_public_key(),
|
||||||
|
"pending_request_count": client.pending_request_count(),
|
||||||
|
"server_url": client.url()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(status.to_string())
|
||||||
|
} else {
|
||||||
|
let status = serde_json::json!({
|
||||||
|
"is_connected": false,
|
||||||
|
"connected_public_key": null,
|
||||||
|
"pending_request_count": 0,
|
||||||
|
"server_url": null
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(status.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all pending requests
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(())` - Requests cleared successfully
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn clear_pending_requests() -> Result<(), JsValue> {
|
||||||
|
SIGSOCKET_CLIENT.with(|c| {
|
||||||
|
let mut client = c.borrow_mut();
|
||||||
|
if let Some(client) = client.as_mut() {
|
||||||
|
client.clear_pending_requests();
|
||||||
|
console_log!("Cleared all pending requests");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user