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        if !receipt.status() {
78            return Err(StrikeError::Contract(format!(
79                "placeOrders reverted (market_id={market_id}, tx={tx_hash}, gas_used={})",
80                receipt.gas_used
81            )));
82        }
83
84        let placed = parse_placed_orders(&receipt, market_id);
85        info!(market_id, tx = %tx_hash, gas_used = receipt.gas_used, placed = placed.len(), "placeOrders confirmed");
86
87        Ok(placed)
88    }
89
90    /// Atomically cancel existing orders and place new ones via `replaceOrders`.
91    ///
92    /// Single transaction: cancels happen first, then placements, with net USDT
93    /// settlement. Zero empty-book time.
94    pub async fn replace(
95        &self,
96        cancel_ids: &[U256],
97        market_id: u64,
98        params: &[OrderParam],
99    ) -> Result<Vec<PlacedOrder>> {
100        self.require_wallet()?;
101
102        let contract_params: Vec<OrderBook::OrderParam> =
103            params.iter().map(|p| p.to_contract_param()).collect();
104
105        let calldata = OrderBook::replaceOrdersCall {
106            cancelIds: cancel_ids.to_vec(),
107            marketId: U256::from(market_id),
108            params: contract_params,
109        }
110        .abi_encode();
111
112        let total_ops = cancel_ids.len() + params.len();
113        let mut tx = TransactionRequest::default()
114            .to(self.config.addresses.order_book)
115            .input(Bytes::from(calldata).into());
116        tx.gas = Some(350_000 * total_ops as u64);
117
118        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
119
120        let tx_hash = *pending.tx_hash();
121        info!(market_id, cancels = cancel_ids.len(), places = params.len(), tx = %tx_hash, "replaceOrders tx sent");
122
123        let receipt = pending
124            .get_receipt()
125            .await
126            .map_err(|e| StrikeError::Contract(e.to_string()))?;
127
128        if !receipt.status() {
129            return Err(StrikeError::Contract(format!(
130                "replaceOrders reverted (market_id={market_id}, tx={tx_hash}, gas_used={})",
131                receipt.gas_used
132            )));
133        }
134
135        let placed = parse_placed_orders(&receipt, market_id);
136        info!(market_id, tx = %tx_hash, gas_used = receipt.gas_used, cancelled = cancel_ids.len(), placed = placed.len(), "replaceOrders confirmed");
137
138        Ok(placed)
139    }
140
141    /// Cancel one or more orders in a single transaction via `cancelOrders`.
142    ///
143    /// Skips already-cancelled orders on-chain (no revert).
144    pub async fn cancel(&self, order_ids: &[U256]) -> Result<()> {
145        self.require_wallet()?;
146
147        if order_ids.is_empty() {
148            return Ok(());
149        }
150
151        let calldata = OrderBook::cancelOrdersCall {
152            orderIds: order_ids.to_vec(),
153        }
154        .abi_encode();
155
156        let mut tx = TransactionRequest::default()
157            .to(self.config.addresses.order_book)
158            .input(Bytes::from(calldata).into());
159        tx.gas = Some(100_000 * order_ids.len() as u64);
160
161        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
162
163        let tx_hash = *pending.tx_hash();
164        info!(count = order_ids.len(), tx = %tx_hash, "cancelOrders tx sent");
165
166        let receipt = pending
167            .get_receipt()
168            .await
169            .map_err(|e| StrikeError::Contract(e.to_string()))?;
170
171        info!(tx = %tx_hash, gas_used = receipt.gas_used, count = order_ids.len(), "cancelOrders confirmed");
172        Ok(())
173    }
174
175    /// Cancel a single order via `cancelOrder`.
176    pub async fn cancel_one(&self, order_id: U256) -> Result<()> {
177        self.require_wallet()?;
178
179        let calldata = OrderBook::cancelOrderCall { orderId: order_id }.abi_encode();
180        let mut tx = TransactionRequest::default()
181            .to(self.config.addresses.order_book)
182            .input(Bytes::from(calldata).into());
183        tx.gas = Some(200_000);
184
185        let pending = send_tx(self.provider, &self.nonce_sender, tx).await?;
186
187        let tx_hash = *pending.tx_hash();
188        let receipt = pending
189            .get_receipt()
190            .await
191            .map_err(|e| StrikeError::Contract(e.to_string()))?;
192
193        info!(order_id = %order_id, tx = %tx_hash, gas_used = receipt.gas_used, "cancelOrder confirmed");
194        Ok(())
195    }
196}
197
198/// Parse `OrderPlaced` events from a transaction receipt.
199fn parse_placed_orders(
200    receipt: &alloy::rpc::types::TransactionReceipt,
201    market_id: u64,
202) -> Vec<PlacedOrder> {
203    let mut placed = Vec::new();
204    for log in receipt.inner.logs() {
205        if let Ok(event) = OrderBook::OrderPlaced::decode_log(&log.inner) {
206            placed.push(PlacedOrder {
207                order_id: event.orderId,
208                side: Side::try_from(event.side).unwrap_or(Side::Bid),
209                market_id,
210            });
211        }
212    }
213    placed
214}