Skip to main content

strike_sdk/
client.rs

1//! StrikeClient — the main entry point for the SDK.
2//!
3//! Supports both read-only mode (no wallet) and trading mode (with wallet).
4//!
5//! # Examples
6//!
7//! ```no_run
8//! use strike_sdk::prelude::*;
9//!
10//! # async fn example() -> strike_sdk::error::Result<()> {
11//! // Read-only
12//! let client = StrikeClient::new(StrikeConfig::bsc_testnet()).build()?;
13//!
14//! // With wallet
15//! let client = StrikeClient::new(StrikeConfig::bsc_testnet())
16//!     .with_private_key("0x...")
17//!     .build()?;
18//! # Ok(())
19//! # }
20//! ```
21
22use alloy::primitives::Address;
23use alloy::providers::{DynProvider, Provider, ProviderBuilder};
24use alloy::signers::local::PrivateKeySigner;
25use std::sync::Arc;
26use tokio::sync::Mutex;
27
28use crate::chain::markets::MarketsClient;
29use crate::chain::orders::OrdersClient;
30use crate::chain::redeem::RedeemClient;
31use crate::chain::tokens::TokensClient;
32use crate::chain::vault::VaultClient;
33use crate::config::StrikeConfig;
34use crate::error::{Result, StrikeError};
35use crate::events::subscribe::EventStream;
36use crate::indexer::client::IndexerClient;
37use crate::nonce::NonceSender;
38
39/// Shared nonce sender reference type.
40pub type NonceSenderRef = Option<Arc<Mutex<NonceSender>>>;
41
42/// Builder for constructing a [`StrikeClient`].
43pub struct StrikeClientBuilder {
44    config: StrikeConfig,
45    rpc_url: Option<String>,
46    wss_url: Option<String>,
47    indexer_url: Option<String>,
48    private_key: Option<String>,
49}
50
51impl StrikeClientBuilder {
52    /// Override the RPC URL from the config.
53    pub fn with_rpc(mut self, url: &str) -> Self {
54        self.rpc_url = Some(url.to_string());
55        self
56    }
57
58    /// Override the WSS URL from the config.
59    pub fn with_wss(mut self, url: &str) -> Self {
60        self.wss_url = Some(url.to_string());
61        self
62    }
63
64    /// Override the indexer URL from the config.
65    pub fn with_indexer(mut self, url: &str) -> Self {
66        self.indexer_url = Some(url.to_string());
67        self
68    }
69
70    /// Set a private key for signing transactions.
71    ///
72    /// Without this, the client operates in read-only mode — event subscriptions
73    /// and queries work, but order placement/cancellation will fail.
74    pub fn with_private_key(mut self, key: &str) -> Self {
75        self.private_key = Some(key.to_string());
76        self
77    }
78
79    /// Build the client.
80    ///
81    /// Connects to the RPC endpoint and optionally configures a signing wallet.
82    pub fn build(self) -> Result<StrikeClient> {
83        let rpc_url = self.rpc_url.unwrap_or(self.config.rpc_url.clone());
84        let wss_url = self.wss_url.unwrap_or(self.config.wss_url.clone());
85        let indexer_url = self.indexer_url.unwrap_or(self.config.indexer_url.clone());
86
87        if rpc_url.is_empty() {
88            return Err(StrikeError::Config("RPC URL is required".into()));
89        }
90
91        let rpc_parsed: reqwest::Url = rpc_url
92            .parse()
93            .map_err(|e| StrikeError::Config(format!("invalid RPC URL: {e}")))?;
94
95        let (provider, signer_addr) = if let Some(key) = &self.private_key {
96            let signer: PrivateKeySigner = key
97                .parse()
98                .map_err(|e| StrikeError::Config(format!("invalid private key: {e}")))?;
99            let addr = signer.address();
100            let wallet = alloy::network::EthereumWallet::from(signer);
101            let p = ProviderBuilder::new()
102                .wallet(wallet)
103                .connect_http(rpc_parsed);
104            (DynProvider::new(p), Some(addr))
105        } else {
106            let p = ProviderBuilder::new().connect_http(rpc_parsed);
107            (DynProvider::new(p), None)
108        };
109
110        Ok(StrikeClient {
111            provider,
112            config: self.config,
113            signer_addr,
114            wss_url,
115            indexer_url,
116            nonce_sender: None,
117        })
118    }
119}
120
121/// The main Strike SDK client.
122///
123/// Provides access to all protocol operations through typed sub-clients:
124/// - [`orders()`](Self::orders) — place, cancel, replace orders
125/// - [`vault()`](Self::vault) — USDT approval and balance
126/// - [`redeem()`](Self::redeem) — redeem outcome tokens
127/// - [`tokens()`](Self::tokens) — outcome token queries
128/// - [`markets()`](Self::markets) — on-chain market reads
129/// - [`events()`](Self::events) — WSS event subscriptions
130/// - [`indexer()`](Self::indexer) — REST indexer client
131pub struct StrikeClient {
132    provider: DynProvider,
133    config: StrikeConfig,
134    signer_addr: Option<Address>,
135    wss_url: String,
136    indexer_url: String,
137    nonce_sender: NonceSenderRef,
138}
139
140impl Clone for StrikeClient {
141    fn clone(&self) -> Self {
142        Self {
143            provider: self.provider.clone(),
144            config: self.config.clone(),
145            signer_addr: self.signer_addr,
146            wss_url: self.wss_url.clone(),
147            indexer_url: self.indexer_url.clone(),
148            nonce_sender: self.nonce_sender.clone(),
149        }
150    }
151}
152
153impl StrikeClient {
154    /// Create a new client builder with the given config.
155    #[allow(clippy::new_ret_no_self)]
156    pub fn new(config: StrikeConfig) -> StrikeClientBuilder {
157        StrikeClientBuilder {
158            config,
159            rpc_url: None,
160            wss_url: None,
161            indexer_url: None,
162            private_key: None,
163        }
164    }
165
166    /// Initialize the shared nonce manager.
167    ///
168    /// Call this once at startup before sending any transactions. All subsequent
169    /// transaction sends (orders, vault approval, redemptions) will route through
170    /// the NonceSender to avoid nonce collisions.
171    ///
172    /// The nonce manager is shared across clones of this client via `Arc`.
173    pub async fn init_nonce_sender(&mut self) -> Result<()> {
174        let signer = self.signer_addr.ok_or(StrikeError::NoWallet)?;
175        let ns = NonceSender::new(self.provider.clone(), signer)
176            .await
177            .map_err(StrikeError::from)?;
178        self.nonce_sender = Some(Arc::new(Mutex::new(ns)));
179        Ok(())
180    }
181
182    /// Get a reference to the shared nonce sender, if initialized.
183    pub fn nonce_sender(&self) -> NonceSenderRef {
184        self.nonce_sender.clone()
185    }
186
187    /// Order placement, cancellation, and replacement.
188    pub fn orders(&self) -> OrdersClient<'_> {
189        OrdersClient::new(
190            &self.provider,
191            self.signer_addr,
192            &self.config,
193            self.nonce_sender.clone(),
194        )
195    }
196
197    /// USDT vault approval and balance queries.
198    pub fn vault(&self) -> VaultClient<'_> {
199        VaultClient::new(
200            &self.provider,
201            self.signer_addr,
202            &self.config,
203            self.nonce_sender.clone(),
204        )
205    }
206
207    /// Outcome token redemption.
208    pub fn redeem(&self) -> RedeemClient<'_> {
209        RedeemClient::new(
210            &self.provider,
211            self.signer_addr,
212            &self.config,
213            self.nonce_sender.clone(),
214        )
215    }
216
217    /// Outcome token balance and approval queries.
218    pub fn tokens(&self) -> TokensClient<'_> {
219        TokensClient::new(&self.provider, self.signer_addr, &self.config)
220    }
221
222    /// On-chain market metadata reads.
223    pub fn markets(&self) -> MarketsClient<'_> {
224        MarketsClient::new(&self.provider, &self.config)
225    }
226
227    /// Subscribe to on-chain events via WSS with auto-reconnect.
228    ///
229    /// Returns an [`EventStream`] that yields [`StrikeEvent`](crate::types::StrikeEvent) items.
230    pub async fn events(&self) -> Result<EventStream> {
231        EventStream::connect(
232            &self.wss_url,
233            self.config.addresses.market_factory,
234            self.config.addresses.batch_auction,
235        )
236        .await
237    }
238
239    /// Scan historical events from chain logs.
240    ///
241    /// Finds orders placed by `owner` that haven't been cancelled.
242    pub async fn scan_orders(
243        &self,
244        from_block: u64,
245        owner: Address,
246    ) -> Result<
247        std::collections::HashMap<
248            u64,
249            (Vec<alloy::primitives::U256>, Vec<alloy::primitives::U256>),
250        >,
251    > {
252        crate::events::scan::scan_live_orders(
253            &self.provider,
254            self.config.addresses.order_book,
255            owner,
256            from_block,
257        )
258        .await
259    }
260
261    /// REST indexer client for startup snapshots.
262    pub fn indexer(&self) -> IndexerClient {
263        IndexerClient::new(&self.indexer_url)
264    }
265
266    /// Get the current block number.
267    pub async fn block_number(&self) -> Result<u64> {
268        self.provider
269            .get_block_number()
270            .await
271            .map_err(StrikeError::Rpc)
272    }
273
274    /// The signer address, if a wallet is configured.
275    pub fn signer_address(&self) -> Option<Address> {
276        self.signer_addr
277    }
278
279    /// The active config.
280    pub fn config(&self) -> &StrikeConfig {
281        &self.config
282    }
283
284    /// The underlying provider (for advanced usage).
285    pub fn provider(&self) -> &DynProvider {
286        &self.provider
287    }
288}