Skip to main content

sdk/signing_client/
mod.rs

1//! High-level signing client for the Real Markets matching engine.
2//!
3//! This module provides the [`SigningClient`] struct, which is a convenience wrapper
4//! around the low-level [`Client`] that automatically handles transaction signing and
5//! provides simplified methods for common operations like submitting and canceling orders.
6//!
7//! The signing client manages cryptographic operations transparently, automatically
8//! generating nonces, signing transactions, and handling the submission workflow.
9//!
10//! # Key Features
11//!
12//! - **Automatic Signing**: All transactions are signed automatically using the configured signer
13//! - **Simplified API**: High-level methods for common operations (submit/cancel orders)
14//! - **Network Presets**: Built-in configurations for testnet and mainnet
15//! - **Batch Operations**: Support for submitting multiple commands in a single transaction
16//! - **Nonce Management**: Automatic nonce generation using system time
17//!
18//! # Network Configuration
19//!
20//! - **Testnet**: [`SigningClient::new_testnet`] for testing and development
21//! - **Mainnet**: [`SigningClient::new_mainnet`] for production trading
22//! - **Custom**: [`SigningClient::new`] for custom network configurations
23//! - **Discovered**: [`SigningClient::builder`] with no `chain_id`, to read it
24//!   from the endpoint itself
25//!
26//! # Examples
27//!
28//! ## Basic Usage
29//! ```rust,no_run
30//! use sdk::{SigningClient, crypto::Signer, types::SubmitOrderRequest};
31//!
32//! #[tokio::main]
33//! async fn main() -> anyhow::Result<()> {
34//!     // a private key in IOTA bech32 form (`iotaprivkey1…`), not hex
35//!     let signer: Signer =
36//!         "iotaprivkey1qrsd2u9ztqr6z2y78sp2ch8kcyak9cx3ezphkpp4acqaqruajxr6qye2645".into();
37//!
38//!     // Create a testnet client
39//!     let client = SigningClient::new_testnet(signer)?;
40//!
41//!     // `market` and `account` are `0x`-prefixed on-chain object ids
42//!     let order = SubmitOrderRequest::builder()
43//!         .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
44//!         .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
45//!         .buy()
46//!         .limit_order("50000.00")
47//!         .quantity("0.1")
48//!         .good_till_cancel()
49//!         .build()?;
50//!
51//!     let report = client.submit_order(order, None).await?;
52//!     println!("Order submitted: {:?}", report);
53//!
54//!     Ok(())
55//! }
56//! ```
57
58use std::{
59    sync::{Arc, Mutex},
60    time::{SystemTime, UNIX_EPOCH},
61};
62
63pub use builder::SigningClientBuilder;
64use networks::Network;
65
66use crate::{
67    client::Client,
68    crypto::Signer,
69    types::{
70        CancelOrderRequest, Command, LiquidatePositionRequest, SetImrRequest, SubmitOrderRequest,
71        SwitchToCrossRequest, SwitchToIsolatedRequest, Transaction, TransactionPayload,
72        TransactionReport,
73    },
74};
75
76mod builder;
77
78/// High-level client that automatically handles transaction signing.
79///
80/// `SigningClient` wraps the low-level [`Client`] and provides a simplified interface
81/// for interacting with the Real Markets matching engine. It automatically handles:
82///
83/// - Transaction signing using the configured [`Signer`]
84/// - Nonce generation for transactions
85/// - Common operations like submitting and canceling orders
86/// - Batch transaction submission
87///
88/// This is the recommended client for most use cases as it reduces boilerplate
89/// and handles cryptographic operations automatically.
90///
91/// # Examples
92///
93/// ```rust,no_run
94/// use sdk::{SigningClient, crypto::Signer};
95///
96/// # fn example() -> anyhow::Result<()> {
97/// // the key is an IOTA bech32 private key (`iotaprivkey1…`), not hex
98/// let signer: Signer =
99///     "iotaprivkey1qrsd2u9ztqr6z2y78sp2ch8kcyak9cx3ezphkpp4acqaqruajxr6qye2645".into();
100/// let client = SigningClient::new_testnet(signer)?;
101///
102/// // Now ready to submit orders, cancel orders, etc.
103/// # Ok(())
104/// # }
105/// ```
106#[derive(Debug, Clone)]
107pub struct SigningClient {
108    http_client: Client,
109    signer: Signer,
110    // `None` means "ask the endpoint" — resolved and cached by the underlying
111    // `Client` on first use. see [`SigningClient::chain_id`].
112    chain_id: Option<String>,
113    nonce: Arc<Mutex<u64>>,
114}
115
116impl SigningClient {
117    /// Creates a new signing client with custom network configuration.
118    ///
119    /// This method allows you to connect to custom or development instances
120    /// of the matching engine by specifying the URL and chain ID manually.
121    ///
122    /// # Arguments
123    ///
124    /// * `signer` - Cryptographic signer for transaction signing
125    /// * `url` - Base URL of the matching engine API endpoint
126    /// * `chain_id` - Chain ID to use for transaction signing (must match the target network)
127    ///
128    /// # Returns
129    ///
130    /// * `Ok(SigningClient)` - Successfully created signing client
131    /// * `Err(_)` - Failed to create the underlying HTTP client
132    ///
133    /// # Examples
134    ///
135    /// ```rust,no_run
136    /// use sdk::{SigningClient, crypto::Signer};
137    ///
138    /// # fn example() -> anyhow::Result<()> {
139    /// let signer: Signer =
140    ///     "iotaprivkey1qrsd2u9ztqr6z2y78sp2ch8kcyak9cx3ezphkpp4acqaqruajxr6qye2645".into();
141    ///
142    /// // Connect to a self-hosted node. The chain id must be the one that node
143    /// // reports over `real_chainId` — `real-t-0001` for a node following
144    /// // testnet — or every transaction is rejected for chain-id mismatch. Leave
145    /// // it unset (`SigningClient::builder`) to have it read from the endpoint.
146    /// let client = SigningClient::new(signer, "http://localhost:1789", "real-t-0001")?;
147    /// # Ok(())
148    /// # }
149    /// ```
150    ///
151    /// # Errors
152    ///
153    /// This method will return an error if the underlying HTTP client cannot be created,
154    /// typically due to invalid URL format.
155    pub fn new(
156        signer: Signer,
157        url: impl AsRef<str>,
158        chain_id: impl ToString,
159    ) -> anyhow::Result<SigningClient> {
160        return Self::builder(signer)
161            .url(url.as_ref())
162            .chain_id(chain_id.to_string())
163            .build();
164    }
165
166    /// Starts building a signing client whose chain id may be left unset, to be
167    /// read from the endpoint instead. See [`SigningClientBuilder`].
168    ///
169    /// ```rust,no_run
170    /// use sdk::{SigningClient, crypto::Signer};
171    ///
172    /// # fn example(signer: Signer) -> anyhow::Result<()> {
173    /// let client = SigningClient::builder(signer)
174    ///     .url("http://localhost:1789")
175    ///     .build()?;
176    /// # Ok(())
177    /// # }
178    /// ```
179    pub fn builder(signer: Signer) -> SigningClientBuilder {
180        return SigningClientBuilder::new(signer);
181    }
182
183    /// creates a signing client reusing an existing `Client` (and thus its underlying HTTP
184    /// connection pool, and its chain-id cache). use this when building many signing clients
185    /// for different signers against the same endpoint — avoids DNS/connection storms.
186    pub fn from_client(
187        http_client: Client,
188        signer: Signer,
189        chain_id: impl ToString,
190    ) -> SigningClient {
191        return builder::assemble(http_client, signer, Some(chain_id.to_string()));
192    }
193
194    /// Creates a new signing client configured for the Real Markets testnet.
195    ///
196    /// This is a convenience method that pre-configures the client with the
197    /// official testnet URL and chain ID. Use this for testing and development.
198    ///
199    /// # Arguments
200    ///
201    /// * `signer` - Cryptographic signer for transaction signing
202    ///
203    /// # Returns
204    ///
205    /// * `Ok(SigningClient)` - Successfully created testnet signing client
206    /// * `Err(_)` - Failed to create the underlying HTTP client
207    ///
208    /// # Examples
209    ///
210    /// ```rust,no_run
211    /// use sdk::{SigningClient, crypto::Signer};
212    ///
213    /// # fn example() -> anyhow::Result<()> {
214    /// let signer: Signer =
215    ///     "iotaprivkey1qrsd2u9ztqr6z2y78sp2ch8kcyak9cx3ezphkpp4acqaqruajxr6qye2645".into();
216    /// let client = SigningClient::new_testnet(signer)?;
217    ///
218    /// // Ready to trade on testnet: https://matching.api.testnet.real.xyz,
219    /// // chain id `real-t-0001`
220    /// # Ok(())
221    /// # }
222    /// ```
223    pub fn new_testnet(signer: Signer) -> anyhow::Result<SigningClient> {
224        return Self::builder(signer)
225            .url(Network::Testnet.matching_url())
226            .chain_id(Network::Testnet.chain_id())
227            .build();
228    }
229
230    /// Creates a new signing client configured for the Real Markets mainnet.
231    ///
232    /// This is a convenience method that pre-configures the client with the
233    /// official mainnet URL and chain ID. Use this for production trading.
234    ///
235    /// **Warning**: This connects to the production network where real value
236    /// is at stake. Ensure your private keys are secure and your code is thoroughly tested.
237    ///
238    /// # Arguments
239    ///
240    /// * `signer` - Cryptographic signer for transaction signing
241    ///
242    /// # Returns
243    ///
244    /// * `Ok(SigningClient)` - Successfully created mainnet signing client
245    /// * `Err(_)` - Failed to create the underlying HTTP client
246    ///
247    /// # Examples
248    ///
249    /// ```rust,no_run
250    /// use sdk::{SigningClient, crypto::Signer};
251    ///
252    /// # fn example() -> anyhow::Result<()> {
253    /// let signer: Signer =
254    ///     "iotaprivkey1qrsd2u9ztqr6z2y78sp2ch8kcyak9cx3ezphkpp4acqaqruajxr6qye2645".into();
255    /// let client = SigningClient::new_mainnet(signer)?;
256    ///
257    /// // Ready for production trading: https://sequencer.api.real.xyz,
258    /// // chain id `real-0001`
259    /// # Ok(())
260    /// # }
261    /// ```
262    pub fn new_mainnet(signer: Signer) -> anyhow::Result<SigningClient> {
263        return Self::builder(signer)
264            .url(Network::Mainnet.matching_url())
265            .chain_id(Network::Mainnet.chain_id())
266            .build();
267    }
268
269    /// Submits a single order to the matching engine.
270    ///
271    /// This is a convenience method that creates a transaction containing a single
272    /// submit order command, signs it, and sends it to the matching engine.
273    ///
274    /// # Arguments
275    ///
276    /// * `order` - The order details to submit (market, price, quantity, etc.)
277    /// * `client_request_id` - Optional unique identifier for tracking this request
278    ///
279    /// # Returns
280    ///
281    /// * `Ok(TransactionReport)` - The engine returned a verdict — which includes
282    ///   rejection. Check `report.success` and the order receipt.
283    /// * `Err(_)` - Signing failed, or no verdict came back at all (transport
284    ///   failure, or a node error carrying no report)
285    ///
286    /// # Examples
287    ///
288    /// ```rust,no_run
289    /// use sdk::{SigningClient, types::SubmitOrderRequest};
290    ///
291    /// # async fn example(client: SigningClient) -> anyhow::Result<()> {
292    /// // Submit a market buy order. `market` and `account` are `0x`-prefixed
293    /// // on-chain object ids — a ticker symbol fails in `build()`.
294    /// let market_order = SubmitOrderRequest::builder()
295    ///     .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
296    ///     .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
297    ///     .buy()
298    ///     .market_order()
299    ///     .quantity("0.1")
300    ///     .immediate_or_cancel()
301    ///     .build()?;
302    ///
303    /// let report = client
304    ///     .submit_order(market_order, Some("req-001".to_string()))
305    ///     .await?;
306    /// println!("Market order result: {:?}", report);
307    ///
308    /// // Submit a limit sell order
309    /// let limit_order = SubmitOrderRequest::builder()
310    ///     .market("0x0f4e0dcd1c3ba1e3f9c25b0e7f9e7d0e6c1a8b3d2e4f5061728394a5b6c7d8e9")
311    ///     .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
312    ///     .sell()
313    ///     .limit_order("3000.00")
314    ///     .quantity("1.0")
315    ///     .good_till_cancel()
316    ///     .build()?;
317    ///
318    /// let report = client.submit_order(limit_order, None).await?;
319    /// println!("Limit order result: {:?}", report);
320    /// # Ok(())
321    /// # }
322    /// ```
323    ///
324    /// # Errors
325    ///
326    /// This method will return an error if:
327    /// - Transaction signing fails
328    /// - Network connection fails
329    /// - The node answered with an error that carried no report
330    ///
331    /// An order the engine *rejects* — failed validation, an unknown market,
332    /// insufficient margin — is not an error: it comes back as `Ok(report)`,
333    /// with the reason in `report.errors` or in the order receipt.
334    pub async fn submit_order(
335        &self,
336        order: SubmitOrderRequest,
337        client_request_id: Option<String>,
338    ) -> anyhow::Result<TransactionReport> {
339        return self
340            .submit_batch([Command::SubmitOrder(order)], client_request_id)
341            .await;
342    }
343
344    /// Cancels a single order in the matching engine.
345    ///
346    /// This method creates a transaction containing a single cancel order command,
347    /// signs it, and sends it to the matching engine for processing.
348    ///
349    /// # Arguments
350    ///
351    /// * `order` - The cancellation request (order ID, account, market, etc.)
352    /// * `client_request_id` - Optional unique identifier for tracking this request
353    ///
354    /// # Returns
355    ///
356    /// * `Ok(TransactionReport)` - The engine returned a verdict — which includes
357    ///   rejection, and also the no-op case where nothing matched the request.
358    ///   Read the cancel receipt to see what was actually terminated.
359    /// * `Err(_)` - Signing failed, or no verdict came back at all
360    ///
361    /// # Examples
362    ///
363    /// ```rust,no_run
364    /// use sdk::{SigningClient, types::CancelOrderRequest};
365    ///
366    /// # async fn example(client: SigningClient) -> anyhow::Result<()> {
367    /// // Cancel one order by its engine order id — a `u128`, not a string.
368    /// // `client_order_id` is the alternative, for ids you minted yourself.
369    /// let cancel_specific = CancelOrderRequest::builder()
370    ///     .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
371    ///     .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
372    ///     .order_id(123456789)
373    ///     .build()?;
374    ///
375    /// let report = client.cancel_order(cancel_specific, None).await?;
376    /// println!("Cancel result: {:?}", report);
377    ///
378    /// // Setting neither id cancels every order the account holds in that market
379    /// let cancel_all = CancelOrderRequest::builder()
380    ///     .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
381    ///     .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
382    ///     .build()?;
383    ///
384    /// let report = client
385    ///     .cancel_order(cancel_all, Some("cancel-all-001".to_string()))
386    ///     .await?;
387    /// println!("Cancel all result: {:?}", report);
388    /// # Ok(())
389    /// # }
390    /// ```
391    ///
392    /// # Errors
393    ///
394    /// This method will return an error if:
395    /// - Transaction signing fails
396    /// - Network connection fails
397    /// - The node answered with an error that carried no report
398    ///
399    /// Cancelling is best-effort at the engine: an order id that does not exist,
400    /// is already filled, or belongs to someone else is logged and skipped, and
401    /// the call still returns `Ok` with that id absent from the cancel receipt.
402    pub async fn cancel_order(
403        &self,
404        order: CancelOrderRequest,
405        client_request_id: Option<String>,
406    ) -> anyhow::Result<TransactionReport> {
407        return self
408            .submit_batch([Command::CancelOrder(order)], client_request_id)
409            .await;
410    }
411
412    /// Asks the matching engine to liquidate a distressed account.
413    ///
414    /// This method creates a transaction containing a single liquidate position command,
415    /// signs it, and sends it to the matching engine for processing.
416    ///
417    /// The request names no market. The engine assesses every market the distressed
418    /// account has a position or orders in and liquidates the ones that breach
419    /// maintenance margin, so this is a trigger rather than an instruction: an
420    /// account that is solvent, has nothing open, or holds a position in a market
421    /// whose mark price is stale is left alone and the call still succeeds.
422    ///
423    /// # Arguments
424    ///
425    /// * `liquidation` - `account` is the caller triggering the liquidation and
426    ///   `distressed_account` the account to be liquidated; both are required
427    /// * `client_request_id` - Optional unique identifier for tracking this request
428    ///
429    /// # Returns
430    ///
431    /// * `Ok(TransactionReport)` - The engine returned a verdict — which includes
432    ///   both rejection and "there was nothing to liquidate"
433    /// * `Err(_)` - Signing failed, or no verdict came back at all
434    ///
435    /// # Examples
436    ///
437    /// ```rust,no_run
438    /// use sdk::{SigningClient, types::LiquidatePositionRequest};
439    ///
440    /// # async fn example(client: SigningClient) -> anyhow::Result<()> {
441    /// // `account` is the liquidator, `distressed_account` the account being
442    /// // liquidated. Both are required and there is no market to name.
443    /// let liquidation = LiquidatePositionRequest::builder()
444    ///     .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
445    ///     .distressed_account(
446    ///         "0x0000000000000000000000000000000000000000000000000000000000000002",
447    ///     )
448    ///     .build()?;
449    ///
450    /// let report = client
451    ///     .liquidate_position(liquidation, Some("liq-001".to_string()))
452    ///     .await?;
453    /// println!("Liquidation result: {:?}", report);
454    /// # Ok(())
455    /// # }
456    /// ```
457    ///
458    /// # Errors
459    ///
460    /// This method will return an error if:
461    /// - Transaction signing fails
462    /// - Network connection fails
463    /// - The node answered with an error that carried no report
464    ///
465    /// Neither a rejected request (an empty `account` or `distressed_account`)
466    /// nor a liquidation that found nothing to do is an error here: the first
467    /// comes back as `Ok(report)` with `report.success == false`, the second as
468    /// an empty `LiquidationReport`.
469    pub async fn liquidate_position(
470        &self,
471        liquidation: LiquidatePositionRequest,
472        client_request_id: Option<String>,
473    ) -> anyhow::Result<TransactionReport> {
474        return self
475            .submit_batch([Command::LiquidatePosition(liquidation)], client_request_id)
476            .await;
477    }
478
479    /// Switches a position to cross-margin mode.
480    ///
481    /// Margin-parameter changes are sequencer-only and must be transmitted as
482    /// a transaction containing exactly one command. The matching engine
483    /// validates and applies state immediately, then relays the change to the
484    /// chain. Subsequent margin-parameter changes for the same account on any
485    /// market are rejected until chain confirmation.
486    pub async fn switch_to_cross(
487        &self,
488        request: SwitchToCrossRequest,
489        client_request_id: Option<String>,
490    ) -> anyhow::Result<TransactionReport> {
491        return self
492            .submit_batch([Command::SwitchToCross(request)], client_request_id)
493            .await;
494    }
495
496    /// Switches a position to isolated-margin mode. See `switch_to_cross` for batching rules.
497    pub async fn switch_to_isolated(
498        &self,
499        request: SwitchToIsolatedRequest,
500        client_request_id: Option<String>,
501    ) -> anyhow::Result<TransactionReport> {
502        return self
503            .submit_batch([Command::SwitchToIsolated(request)], client_request_id)
504            .await;
505    }
506
507    /// Sets the position-level IMR for an account/market. See `switch_to_cross` for batching rules.
508    pub async fn set_imr(
509        &self,
510        request: SetImrRequest,
511        client_request_id: Option<String>,
512    ) -> anyhow::Result<TransactionReport> {
513        return self
514            .submit_batch([Command::SetImr(request)], client_request_id)
515            .await;
516    }
517
518    /// Submits a batch of commands in a single transaction.
519    ///
520    /// This method allows you to group multiple operations (orders, cancellations, etc.)
521    /// into a single transaction, which is more efficient than sending individual
522    /// transactions and ensures atomic execution of all commands.
523    ///
524    /// The method automatically generates a nonce using the current system time,
525    /// signs the transaction, and submits it to the matching engine.
526    ///
527    /// # Arguments
528    ///
529    /// * `commands` - An iterable collection of commands to execute
530    /// * `client_request_id` - Optional unique identifier for tracking this transaction
531    ///
532    /// # Returns
533    ///
534    /// * `Ok(TransactionReport)` - The engine returned a verdict for the batch,
535    ///   accepted or rejected; `report.receipts` holds one receipt per command
536    ///   that ran and `report.errors` is keyed by command index
537    /// * `Err(_)` - Signing failed, or no verdict came back at all
538    ///
539    /// # Examples
540    ///
541    /// ```rust,no_run
542    /// use sdk::{
543    ///     SigningClient,
544    ///     types::{CancelOrderRequest, Command, SubmitOrderRequest},
545    /// };
546    ///
547    /// # async fn example(client: SigningClient) -> anyhow::Result<()> {
548    /// // Quote both sides of one market in a single transaction
549    /// let commands = vec![
550    ///     Command::SubmitOrder(
551    ///         SubmitOrderRequest::builder()
552    ///             .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
553    ///             .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
554    ///             .buy()
555    ///             .limit_order("49000.00")
556    ///             .quantity("0.1")
557    ///             .good_till_cancel()
558    ///             .build()?,
559    ///     ),
560    ///     Command::SubmitOrder(
561    ///         SubmitOrderRequest::builder()
562    ///             .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
563    ///             .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
564    ///             .sell()
565    ///             .limit_order("51000.00")
566    ///             .quantity("0.1")
567    ///             .good_till_cancel()
568    ///             .build()?,
569    ///     ),
570    /// ];
571    ///
572    /// let report = client
573    ///     .submit_batch(commands, Some("batch-001".to_string()))
574    ///     .await?;
575    /// println!("Batch result: {:?}", report);
576    ///
577    /// // Cancel every resting order in the market, then take the offer.
578    /// // Omitting both ids on the cancel is what makes it a cancel-all.
579    /// let commands = vec![
580    ///     Command::CancelOrder(
581    ///         CancelOrderRequest::builder()
582    ///             .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
583    ///             .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
584    ///             .build()?,
585    ///     ),
586    ///     Command::SubmitOrder(
587    ///         SubmitOrderRequest::builder()
588    ///             .market("0x5efc5ad00c209baf4c5d77dfd1be18e8b50916e53adbe2142ae1b47af445303c")
589    ///             .account("0xcc6ebf909253ea35314bb28d91321f6415827c01790fba84aaf570c87372850a")
590    ///             .buy()
591    ///             .market_order()
592    ///             .quantity("1.0")
593    ///             .immediate_or_cancel()
594    ///             .build()?,
595    ///     ),
596    /// ];
597    ///
598    /// let report = client.submit_batch(commands, None).await?;
599    /// # Ok(())
600    /// # }
601    /// ```
602    ///
603    /// # Atomicity
604    ///
605    /// Admission is all-or-nothing: if any command fails validation the whole
606    /// transaction is rejected and nothing runs. Execution is not. The engine
607    /// walks the batch in order and each command yields its own receipt, so a
608    /// cancel that succeeded stays cancelled even if the order after it is
609    /// rejected. There is no rollback — read every receipt, not just
610    /// `report.success`.
611    ///
612    /// One exception to batching at all: the three margin-parameter commands
613    /// (`switch_to_cross`, `switch_to_isolated`, `set_imr`) must be alone in a
614    /// transaction, and a batch containing one alongside anything else is
615    /// rejected outright.
616    ///
617    /// # Nonce Management
618    ///
619    /// The method automatically generates a unique nonce using microseconds since
620    /// Unix epoch. This ensures transaction uniqueness and prevents replay attacks.
621    ///
622    /// # Errors
623    ///
624    /// This method will return an error if:
625    /// - Transaction signing fails
626    /// - Network connection fails
627    /// - The node answered with an error that carried no report
628    /// - The chain id has to be discovered and the endpoint cannot be reached
629    ///
630    /// A batch the engine *rejects* — a command that fails validation, or a
631    /// margin-parameter command batched with others — is not an error: it comes
632    /// back as `Ok(report)` with `report.errors` keyed by command index. A
633    /// system clock before the Unix epoch panics rather than erroring.
634    pub async fn submit_batch<I>(
635        &self,
636        commands: I,
637        client_request_id: Option<String>,
638    ) -> anyhow::Result<TransactionReport>
639    where
640        I: IntoIterator<Item = Command>,
641    {
642        let payload = TransactionPayload::builder()
643            .nonce(self.next_nonce())
644            .chain_id(self.chain_id().await?)
645            .commands(commands)
646            .build()?;
647
648        let mut transaction_builder = Transaction::builder().payload(payload);
649        if let Some(cid) = client_request_id {
650            transaction_builder = transaction_builder.client_request_id(cid);
651        }
652        let transaction = transaction_builder.sign_and_build(self.signer.clone())?;
653
654        return self.http_client.send_transaction(transaction).await;
655    }
656
657    /// The chain id this client signs with.
658    ///
659    /// If one was supplied at construction it is returned as-is. Otherwise it is
660    /// read from the endpoint and cached on the underlying [`Client`], so only
661    /// the first call costs a round-trip. Every transaction resolves its chain id
662    /// through here.
663    ///
664    /// ```rust,no_run
665    /// use sdk::{SigningClient, crypto::Signer};
666    ///
667    /// # async fn example(signer: Signer, url: &str) -> anyhow::Result<()> {
668    /// let client = SigningClient::builder(signer).url(url).build()?;
669    /// println!("trading on {}", client.chain_id().await?);
670    /// # Ok(())
671    /// # }
672    /// ```
673    ///
674    /// # Errors
675    ///
676    /// Returns an error only in the discovered case, if the endpoint is
677    /// unreachable or its response cannot be parsed.
678    pub async fn chain_id(&self) -> anyhow::Result<&str> {
679        return match &self.chain_id {
680            Some(chain_id) => Ok(&**chain_id),
681            None => self.http_client.get_chain_id().await,
682        };
683    }
684
685    /// Asserts that the endpoint is the network this client was configured for.
686    ///
687    /// Call this once at startup. A client built with an explicit chain id will
688    /// otherwise sign for whatever network its configured id names, even when
689    /// pointed at a different endpoint entirely — so a wrong URL silently
690    /// produces transactions for the wrong network. This turns that into a boot
691    /// failure.
692    ///
693    /// ```rust,no_run
694    /// use sdk::{SigningClient, crypto::Signer};
695    ///
696    /// # async fn example(signer: Signer, url: &str) -> anyhow::Result<()> {
697    /// let client = SigningClient::new(signer, url, "real-t-0001")?;
698    /// client.verify_chain_id().await?; // refuses to continue on the wrong network
699    /// # Ok(())
700    /// # }
701    /// ```
702    ///
703    /// # Errors
704    ///
705    /// Fails if the endpoint reports a different chain id, or is unreachable, or
706    /// if this client has no configured chain id to check against — a client
707    /// that discovers its chain id cannot be verified, and reporting success
708    /// would be a false assurance (it would also mask a config value that
709    /// arrived empty).
710    pub async fn verify_chain_id(&self) -> anyhow::Result<()> {
711        let Some(configured) = &self.chain_id else {
712            anyhow::bail!(
713                "cannot verify chain id: none was configured, this client reads it \
714                 from the endpoint — set one to assert which network you expect"
715            );
716        };
717
718        return self.http_client.verify_chain_id(configured).await;
719    }
720
721    /// Retrieves the version information of the matching engine.
722    ///
723    /// This method queries the matching engine to get its current version string,
724    /// which is useful for compatibility checking and debugging. It delegates
725    /// to the underlying HTTP client.
726    ///
727    /// # Returns
728    ///
729    /// * `Ok(String)` - The version string of the matching engine
730    /// * `Err(_)` - Network error or failed to parse response
731    ///
732    /// # Examples
733    ///
734    /// ```rust,no_run
735    /// use sdk::SigningClient;
736    ///
737    /// # async fn example(client: SigningClient) -> anyhow::Result<()> {
738    /// let version = client.get_version().await?;
739    /// println!("Engine version: {}", version);
740    /// # Ok(())
741    /// # }
742    /// ```
743    pub async fn get_version(&self) -> anyhow::Result<String> {
744        return self.http_client.get_version().await;
745    }
746
747    /// Performs a health check on the matching engine.
748    ///
749    /// This method pings the matching engine to verify it's running and responsive.
750    /// It's useful for monitoring, load balancer health checks, and ensuring
751    /// connectivity before attempting to send transactions. It delegates to
752    /// the underlying HTTP client.
753    ///
754    /// # Returns
755    ///
756    /// * `Ok(HealthCheck)` - The node answered. `status` is a constant `true` on
757    ///   any node that answers at all, so the liveness signal is the other four
758    ///   fields: `sequencer_height` / `sequencer_ts` (the last block the engine
759    ///   opened and its unix-millisecond timestamp) and `settlement_chain_height`
760    ///   / `settlement_chain_ts` (the last settlement-chain checkpoint ingested).
761    ///   A node that has not produced a block yet reports zeros.
762    /// * `Err(_)` - Network error or failed to reach the matching engine
763    ///
764    /// # Examples
765    ///
766    /// ```rust,no_run
767    /// use sdk::{SigningClient, types::SubmitOrderRequest};
768    ///
769    /// # async fn example(client: SigningClient, order: SubmitOrderRequest) -> anyhow::Result<()> {
770    /// // Check health before trading. `status` is always true on a node that
771    /// // answers, so what tells you the engine is live is the heights moving.
772    /// let health = client.healthcheck().await?;
773    /// println!(
774    ///     "sequencer {}@{} ms, settlement chain {}@{} ms",
775    ///     health.sequencer_height,
776    ///     health.sequencer_ts,
777    ///     health.settlement_chain_height,
778    ///     health.settlement_chain_ts,
779    /// );
780    ///
781    /// client.submit_order(order, None).await?;
782    /// # Ok(())
783    /// # }
784    /// ```
785    pub async fn healthcheck(&self) -> anyhow::Result<crate::client::HealthCheck> {
786        return self.http_client.healthcheck().await;
787    }
788
789    /// Generates the next unique nonce for transaction submission.
790    ///
791    /// This internal method generates monotonically increasing nonces based on
792    /// the current system time in microseconds. It handles concurrent access
793    /// by incrementing the nonce if the current time is not greater than the
794    /// last generated nonce.
795    ///
796    /// # Thread Safety
797    ///
798    /// This method is thread-safe and can be called concurrently. It uses a
799    /// mutex to ensure that each transaction gets a unique nonce even when
800    /// multiple threads are submitting transactions simultaneously.
801    ///
802    /// # Returns
803    ///
804    /// A unique nonce value (microseconds since Unix epoch, monotonically increasing)
805    fn next_nonce(&self) -> u64 {
806        let mut nonce = self.nonce.lock().unwrap();
807        let now = Self::now();
808
809        // in case we are accessing in parallel
810        // the nonce is already used
811        if *nonce >= now {
812            *nonce += 1;
813        } else {
814            *nonce = now;
815        }
816
817        return *nonce;
818    }
819
820    /// Returns the current time as microseconds since Unix epoch.
821    ///
822    /// This internal helper method is used for nonce generation. It converts
823    /// the system time to microseconds to provide high-resolution timestamps
824    /// for transaction uniqueness.
825    ///
826    /// # Returns
827    ///
828    /// Current time as microseconds since Unix epoch (u64)
829    ///
830    /// # Panics
831    ///
832    /// Panics if the system time is before the Unix epoch, which should never
833    /// happen on properly configured systems.
834    fn now() -> u64 {
835        return SystemTime::now()
836            .duration_since(UNIX_EPOCH)
837            .unwrap()
838            .as_micros() as u64;
839    }
840}