Skip to main content

strike_sdk/chain/
orders.rs

1//! Order placement, cancellation, and replacement on the OrderBook contract.
2
3use alloy::primitives::{Address, Bytes, U256};
4use alloy::providers::DynProvider;
5use alloy::rpc::types::TransactionRequest;
6use alloy::sol_types::{SolCall, SolEvent};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9use tracing::info;
10
11use crate::chain::send_tx;
12use crate::config::StrikeConfig;
13use crate::contracts::OrderBook;
14use crate::error::{Result, StrikeError};
15use crate::nonce::NonceSender;
16use crate::types::{OrderParam, PlacedOrder, Side};
17
18/// Client for order operations on the OrderBook contract.
19pub struct OrdersClient<'a> {
20    provider: &'a DynProvider,
21    signer_addr: Option<Address>,
22    config: &'a StrikeConfig,
23    nonce_sender: Option<Arc<Mutex<NonceSender>>>,
24}
25
26impl<'a> OrdersClient<'a> {
27    pub(crate) fn new(
28        provider: &'a DynProvider,
29        signer_addr: Option<Address>,
30        config: &'a StrikeConfig,
31        nonce_sender: Option<Arc<Mutex<NonceSender>>>,
32    ) -> Self {
33        Self {
34            provider,
35            signer_addr,
36            config,
37            nonce_sender,
38        }
39    }
40
41    fn require_wallet(&self) -> Result<Address> {
42        self.signer_addr.ok_or(StrikeError::NoWallet)
43    }
44
45    /// Place one or more orders on a market in a single transaction.
46    ///
47    /// Uses `placeOrders(marketId, OrderParam[])`. Returns placed orders with
48    /// their assigned on-chain IDs (parsed from `OrderPlaced` events in the receipt).
49    pub async fn place(&self, market_id: u64, params: &[OrderParam]) -> Result<Vec<PlacedOrder>> {
50        self.require_wallet()?;
51
52        let contract_params: Vec<OrderBook::OrderParam> =
53            params.iter().map(|p| p.to_contract_param()).collect();
54
55        let calldata = OrderBook::placeOrdersCall {
56            marketId: U256::from(market_id),
57            params: contract_params,
58        }
59        .abi_encode();
60
61        let order_count = params.len();
62        let mut tx = TransactionRequest::default()
63            .to(self.config.addresses.order_book)
64            .input(Bytes::from(calldata).into());
65        tx.gas = Some(350_000 * order_count as u64);
66
67        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
68
69        let tx_hash = *pending.tx_hash();
70        info!(market_id, order_count, tx = %tx_hash, "placeOrders tx sent");
71
72        let receipt = pending
73            .get_receipt()
74            .await
75            .map_err(|e| StrikeError::Contract(e.to_string()))?;
76
77        let placed = parse_placed_orders(&receipt, market_id);
78        info!(market_id, tx = %tx_hash, gas_used = receipt.gas_used, placed = placed.len(), "placeOrders confirmed");
79
80        Ok(placed)
81    }
82
83    /// Atomically cancel existing orders and place new ones via `replaceOrders`.
84    ///
85    /// Single transaction: cancels happen first, then placements, with net USDT
86    /// settlement. Zero empty-book time.
87    pub async fn replace(
88        &self,
89        cancel_ids: &[U256],
90        market_id: u64,
91        params: &[OrderParam],
92    ) -> Result<Vec<PlacedOrder>> {
93        self.require_wallet()?;
94
95        let contract_params: Vec<OrderBook::OrderParam> =
96            params.iter().map(|p| p.to_contract_param()).collect();
97
98        let calldata = OrderBook::replaceOrdersCall {
99            cancelIds: cancel_ids.to_vec(),
100            marketId: U256::from(market_id),
101            params: contract_params,
102        }
103        .abi_encode();
104
105        let total_ops = cancel_ids.len() + params.len();
106        let mut tx = TransactionRequest::default()
107            .to(self.config.addresses.order_book)
108            .input(Bytes::from(calldata).into());
109        tx.gas = Some(350_000 * total_ops as u64);
110
111        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
112
113        let tx_hash = *pending.tx_hash();
114        info!(market_id, cancels = cancel_ids.len(), places = params.len(), tx = %tx_hash, "replaceOrders tx sent");
115
116        let receipt = pending
117            .get_receipt()
118            .await
119            .map_err(|e| StrikeError::Contract(e.to_string()))?;
120
121        let placed = parse_placed_orders(&receipt, market_id);
122        info!(market_id, tx = %tx_hash, gas_used = receipt.gas_used, cancelled = cancel_ids.len(), placed = placed.len(), "replaceOrders confirmed");
123
124        Ok(placed)
125    }
126
127    /// Cancel one or more orders in a single transaction via `cancelOrders`.
128    ///
129    /// Skips already-cancelled orders on-chain (no revert).
130    pub async fn cancel(&self, order_ids: &[U256]) -> Result<()> {
131        self.require_wallet()?;
132
133        if order_ids.is_empty() {
134            return Ok(());
135        }
136
137        let calldata = OrderBook::cancelOrdersCall {
138            orderIds: order_ids.to_vec(),
139        }
140        .abi_encode();
141
142        let mut tx = TransactionRequest::default()
143            .to(self.config.addresses.order_book)
144            .input(Bytes::from(calldata).into());
145        tx.gas = Some(100_000 * order_ids.len() as u64);
146
147        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
148
149        let tx_hash = *pending.tx_hash();
150        info!(count = order_ids.len(), tx = %tx_hash, "cancelOrders tx sent");
151
152        let receipt = pending
153            .get_receipt()
154            .await
155            .map_err(|e| StrikeError::Contract(e.to_string()))?;
156
157        info!(tx = %tx_hash, gas_used = receipt.gas_used, count = order_ids.len(), "cancelOrders confirmed");
158        Ok(())
159    }
160
161    /// Cancel a single order via `cancelOrder`.
162    pub async fn cancel_one(&self, order_id: U256) -> Result<()> {
163        self.require_wallet()?;
164
165        let calldata = OrderBook::cancelOrderCall { orderId: order_id }.abi_encode();
166        let mut tx = TransactionRequest::default()
167            .to(self.config.addresses.order_book)
168            .input(Bytes::from(calldata).into());
169        tx.gas = Some(200_000);
170
171        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
172
173        let tx_hash = *pending.tx_hash();
174        let receipt = pending
175            .get_receipt()
176            .await
177            .map_err(|e| StrikeError::Contract(e.to_string()))?;
178
179        info!(order_id = %order_id, tx = %tx_hash, gas_used = receipt.gas_used, "cancelOrder confirmed");
180        Ok(())
181    }
182}
183
184/// Parse `OrderPlaced` events from a transaction receipt.
185fn parse_placed_orders(
186    receipt: &alloy::rpc::types::TransactionReceipt,
187    market_id: u64,
188) -> Vec<PlacedOrder> {
189    let mut placed = Vec::new();
190    for log in receipt.inner.logs() {
191        if let Ok(event) = OrderBook::OrderPlaced::decode_log(&log.inner) {
192            placed.push(PlacedOrder {
193                order_id: event.orderId,
194                side: Side::try_from(event.side).unwrap_or(Side::Bid),
195                market_id,
196            });
197        }
198    }
199    placed
200}