diff --git a/Cargo.lock b/Cargo.lock index 46df5f4a..46754b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2588,7 +2588,7 @@ dependencies = [ [[package]] name = "express-relay-client" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ethers", "express-relay-api-types", diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index d6c67561..b89c3118 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "express-relay-client" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "Pyth Express Relay client" repository = "https://github.com/pyth-network/per" diff --git a/sdk/rust/src/evm.rs b/sdk/rust/src/evm.rs index 9384b32c..56724a19 100644 --- a/sdk/rust/src/evm.rs +++ b/sdk/rust/src/evm.rs @@ -57,6 +57,20 @@ pub struct Config { pub chain_id_num: u64, } + +/// Retrieves the EVM configuration for a specific chain. +/// +/// # Arguments +/// +/// * `chain_id` - A string slice representing the blockchain chain ID. +/// +/// # Returns +/// +/// * `Result` - A result containing the configuration or an error if the chain is unsupported. +/// +/// # Errors +/// +/// Returns `ClientError::ChainNotSupported` if the chain ID is unrecognized. pub fn get_config(chain_id: &str) -> Result { match chain_id { "mode" => Ok(Config { @@ -99,6 +113,20 @@ pub fn get_config(chain_id: &str) -> Result { } } +/// Constructs the Permit2 compatible permitted tokens list for a given opportunity and bid parameters. +/// +/// # Arguments +/// +/// * `opportunity` - The EVM opportunity structure. +/// * `bid_params` - Bid parameters. +/// +/// # Returns +/// +/// * `Result, ClientError>` - A list of token permissions or an error. +/// +/// # Errors +/// +/// Returns an error if the configuration for the chain cannot be retrieved. pub fn make_permitted_tokens( opportunity: OpportunityEvm, bid_params: BidParamsEvm, @@ -130,6 +158,21 @@ pub fn make_permitted_tokens( Ok(permitted_tokens) } +/// Creates execution parameters required for executing an opportunity through the ER contract. +/// +/// # Arguments +/// +/// * `opportunity` - The EVM opportunity structure. +/// * `bid_params` - Bid parameters. +/// * `executor` - The address of the executor. +/// +/// # Returns +/// +/// * `Result` - Execution parameters including permits and witness details. +/// +/// # Errors +/// +/// Returns an error if permit2 compatible permitted tokens cannot be constructed. pub fn make_opportunity_execution_params( opportunity: OpportunityEvm, bid_params: BidParamsEvm, @@ -260,6 +303,21 @@ fn get_signature( .map_err(|e| ClientError::NewBidError(format!("Failed to sign eip712 data: {:?}", e))) } +/// Generates adapter calldata for executing an opportunity. +/// +/// # Arguments +/// +/// * `opportunity` - The EVM opportunity structure. +/// * `bid_params` - Bid parameters. +/// * `wallet` - A `LocalWallet` object for signing transactions. +/// +/// # Returns +/// +/// * `Result` - The calldata bytes for the opportunity adapter. +/// +/// # Errors +/// +/// Returns an error if signature generation or execution parameter creation fails. pub fn make_adapter_calldata( opportunity: OpportunityEvm, bid_params: BidParamsEvm, @@ -278,6 +336,15 @@ pub fn make_adapter_calldata( Ok(calldata.into()) } +/// Retrieves opportunity parameters from an `OpportunityEvm` object. +/// +/// # Arguments +/// +/// * `opportunity` - The EVM opportunity structure. +/// +/// # Returns +/// +/// * `OpportunityCreateV1Evm` - The extracted opportunity parameters. pub fn get_params(opportunity: OpportunityEvm) -> OpportunityCreateV1Evm { let OpportunityParamsEvm::V1(OpportunityParamsV1Evm(params)) = opportunity.params; params diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index 939b53e1..a5c7999d 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -145,6 +145,21 @@ enum MessageType { Update(api_types::ws::ServerUpdateResponse), } +/// A stream of WebSocket updates received from the server. +/// +/// # Developer Notes +/// +/// - This struct wraps a `BroadcastStream` that delivers updates as `ServerUpdateResponse` objects. +/// - The `PhantomData` ensures that the lifetime of this stream is explicitly tied to the `WsClient` instance. +/// +/// ## Why PhantomData? +/// +/// - `PhantomData<&'a ()>` acts as a marker to indicate that this struct's lifetime `'a` +/// depends on the `WsClient` that created it. +/// - Without `PhantomData`, the compiler cannot ensure that the `WsClientUpdateStream` does not outlive +/// the `WsClient`. This can lead to dangling references or invalid state. +/// - By including `PhantomData`, the borrow checker guarantees at compile time that the stream +/// remains valid only as long as the `WsClient` exists. pub struct WsClientUpdateStream<'a> { stream: BroadcastStream, _lifetime: PhantomData<&'a ()>, @@ -170,12 +185,40 @@ impl Stream for WsClientUpdateStream<'_> { } impl WsClient { + /// Retrieves a stream of WebSocket updates from the server. + /// + /// # Returns + /// + /// * `WsClientUpdateStream` - A stream of updates that can be polled asynchronously. + /// + /// # Lifetime + /// + /// The lifetime of the update stream is guaranteed at compile time to be tied to the `WsClient`. + /// If the `WsClient` is dropped, the stream will also become invalid. pub fn get_update_stream(&self) -> WsClientUpdateStream { WsClientUpdateStream::new(BroadcastStream::new( self.inner.update_receiver.resubscribe(), )) } + /// Runs the WebSocket event loop, managing incoming messages, outgoing requests, and connection health. + /// + /// # Developer Notes + /// + /// - This function runs continuously and listens for three main events: + /// 1. **Incoming WebSocket messages**: Handles text, binary, ping, and close frames. + /// - WebSocket messages can be of two types: + /// - **Updates**: Broadcasted to all clients via the `update_sender` channel. + /// - **Responses**: Sent as a response to a specific client request and delivered to the + /// corresponding `oneshot` channel for that request (tracked via `requests_map`). + /// 2. **Requests from the client**: Sends messages through the WebSocket when received from the request channel. + /// 3. **Connection health check**: Monitors for pings to ensure the connection is alive. + /// + /// - Uses a `HashMap` (`requests_map`) to track pending requests and match responses based on their IDs. + /// - If no ping is received for 32 seconds, the function assumes the connection is broken and terminates. + /// + /// This function is spawned as a background task and must be resilient to message errors + /// or other intermittent failures. async fn run( mut ws_stream: WebSocketStream>, mut request_receiver: mpsc::UnboundedReceiver, @@ -258,6 +301,18 @@ impl WsClient { *write_guard } + /// Sends a WebSocket message and waits for a response. + /// + /// # Developer Notes + /// + /// - Generates a unique request ID using `fetch_add_request_id` to match requests with responses. + /// - Sends a `ClientRequest` message through the internal `request_sender` channel. + /// - Uses a `oneshot` channel to wait for the response corresponding to the request ID. + /// - Times out after 5 seconds if no response is received, returning a `WsRequestFailed` error. + /// + /// **Request Matching**: + /// Responses are matched to their corresponding requests via the `requests_map` in the `run` loop. + /// If the timeout occurs, developers must ensure that orphaned requests are handled appropriately. async fn send( &self, message: api_types::ws::ClientMessage, @@ -294,6 +349,19 @@ impl WsClient { } } + /// Subscribes to updates for specific blockchain chains. + /// + /// # Arguments + /// + /// * `chain_ids` - A vector of chain IDs as strings. + /// + /// # Returns + /// + /// * `Result<(), ClientError>` - Returns `Ok(())` on success or an error. + /// + /// # Errors + /// + /// Returns an error if the subscription request fails or times out. pub async fn chain_subscribe(&self, chain_ids: Vec) -> Result<(), ClientError> { let message = api_types::ws::ClientMessage::Subscribe { chain_ids: chain_ids @@ -308,6 +376,19 @@ impl WsClient { } } + /// Unsubscribes from updates for specific blockchain chains. + /// + /// # Arguments + /// + /// * `chain_ids` - A vector of chain IDs as strings. + /// + /// # Returns + /// + /// * `Result<(), ClientError>` - Returns `Ok(())` on success or an error. + /// + /// # Errors + /// + /// Returns an error if the unsubscription request fails or times out. pub async fn chain_unsubscribe(&self, chain_ids: Vec) -> Result<(), ClientError> { let message = api_types::ws::ClientMessage::Unsubscribe { chain_ids: chain_ids @@ -322,6 +403,19 @@ impl WsClient { } } + /// Submits a bid to the server. + /// + /// # Arguments + /// + /// * `bid` - The bid object to be submitted, which contains the relevant parameters for the transaction. + /// + /// # Returns + /// + /// * `Result` - The result of the bid submission. + /// + /// # Errors + /// + /// Returns an error if the WebSocket request fails or the server responds with an error. pub async fn submit_bid( &self, bid: api_types::bid::BidCreate, @@ -350,6 +444,31 @@ impl Client { } } + /// Sends an HTTP request to the server and decodes the response. + /// + /// # Developer Notes + /// + /// - Constructs an HTTP request using the specified route and optional query parameters. + /// - If an `api_key` is set, it adds a `Bearer` authorization header to the request. + /// - This function expects the server response to conform to the following structure: + /// - `DecodedResponse::Ok` for successful responses. + /// - `DecodedResponse::Err` for error bodies returned by the server. + /// - The function uses `reqwest::Client` internally and decodes the response using `serde`. + /// + /// # Parameters + /// + /// - `route` - Defines the API endpoint and HTTP method via the `Routable` trait. + /// - `query` - Optional query parameters that are serialized into the request URL. + /// + /// # Implementation Details + /// + /// - If the HTTP response is valid but contains an error body, the function returns a + /// `ClientError::RequestError` with the server's error message. + /// - If the HTTP response fails to decode, it returns `ClientError::DecodeResponseFailed`. + /// - Errors due to request failure (e.g., network issues) are returned as `ClientError::RequestFailed`. + /// + /// **Timeouts**: + /// The default `reqwest` client timeout applies here. Ensure proper timeout handling in the caller. async fn send( &self, route: impl Routable, @@ -371,6 +490,19 @@ impl Client { Client::decode(response).await } + /// Creates a new HTTP client with the provided configuration. + /// + /// # Arguments + /// + /// * `config` - The client configuration containing HTTP URL and optional API key. + /// + /// # Returns + /// + /// * `Result` - A result containing the initialized client or an error. + /// + /// # Errors + /// + /// Returns an error if the HTTP URL is invalid or has an unsupported scheme. pub fn try_new(config: ClientConfig) -> Result { let http_url = Url::parse(config.http_url.as_str()) .map_err(|e| ClientError::InvalidHttpUrl(e.to_string()))?; @@ -397,6 +529,19 @@ impl Client { }) } + /// Establishes a WebSocket connection to the server. + /// + /// # Returns + /// + /// * `Result` - A thread-safe WebSocket client for interacting with the server. + /// + /// # Errors + /// + /// Returns an error if the connection or WebSocket handshake fails. + /// + /// # Thread Safety + /// + /// The returned `WsClient` is thread-safe and can be cloned to share across multiple tasks. pub async fn connect_websocket(&self) -> Result { let url = self .ws_url @@ -432,6 +577,15 @@ impl Client { }) } + /// Fetches opportunities based on optional query parameters. + /// + /// # Arguments + /// + /// * `params` - Optional query parameters for filtering opportunities. + /// + /// # Returns + /// + /// * `Result, ClientError>` - A list of opportunities or an error. pub async fn get_opportunities( &self, params: Option, @@ -439,6 +593,21 @@ impl Client { self.send(Route::GetOpportunities, params).await } + /// Creates a new bid for an opportunity. + /// + /// # Type Parameters + /// + /// * `T` - A type that implements the `Biddable` trait. + /// + /// # Arguments + /// + /// * `opportunity` - The opportunity to bid on. + /// * `params` - Bid parameters specific to the opportunity type. + /// * `private_key` - The private key for signing the bid. + /// + /// # Returns + /// + /// * `Result` - A bid creation object or an error. pub async fn new_bid( opportunity: T, params: T::Params,