renegade_sdk/renegade_wallet_client/actions/
place_order.rs

1//! Places an order
2
3use std::str::FromStr;
4
5use alloy::primitives::Address;
6use renegade_circuit_types::{Amount, fixed_point::FixedPoint};
7use renegade_crypto::fields::scalar_to_u256;
8use renegade_darkpool_types::{
9    balance::{DarkpoolBalance, DarkpoolStateBalance},
10    intent::DarkpoolStateIntent,
11};
12use renegade_external_api::{
13    http::order::{CREATE_ORDER_ROUTE, CreateOrderRequest, CreateOrderResponse},
14    types::{
15        ApiIntent, ApiOrderCore, ApiPublicIntentPermit, OrderAuth, OrderType,
16        SignatureWithNonce as ApiSignatureWithNonce,
17    },
18};
19use renegade_solidity_abi::v2::IDarkpoolV2::{PublicIntentPermit, SignatureWithNonce};
20use uuid::Uuid;
21
22use crate::{
23    RenegadeClientError,
24    actions::{NON_BLOCKING_PARAM, construct_http_path},
25    client::RenegadeClient,
26    utils::unwrap_field,
27    websocket::{DEFAULT_TASK_TIMEOUT, TaskWaiter},
28};
29
30// --- Public Actions --- //
31impl RenegadeClient {
32    /// Create a new order builder with the client's account address as the
33    /// owner
34    pub fn new_order_builder(&self) -> OrderBuilder {
35        OrderBuilder::new(self.get_account_address())
36    }
37
38    /// Places an order via the relayer. Waits for the order creation task to
39    /// complete before returning the created order.
40    ///
41    /// Orders will only be committed to onchain state upon their first fill.
42    /// As such, this method alone just registers this order as an intent to
43    /// trade with the relayer.
44    pub async fn place_order(&self, built_order: BuiltOrder) -> Result<(), RenegadeClientError> {
45        let request = self.build_create_order_request(built_order).await?;
46
47        let path = self.build_create_order_request_path(false)?;
48
49        self.relayer_client.post::<_, CreateOrderResponse>(&path, request).await?;
50
51        Ok(())
52    }
53
54    /// Enqueues an order placement task in the relayer. Returns the expected
55    /// order to be created, and a `TaskWaiter` that can be used to await task
56    /// completion.
57    ///
58    /// Orders will only be committed to onchain state upon their first fill.
59    /// As such, this method alone just registers this order as an intent to
60    /// trade with the relayer.
61    pub async fn enqueue_order_placement(
62        &self,
63        built_order: BuiltOrder,
64    ) -> Result<TaskWaiter, RenegadeClientError> {
65        let request = self.build_create_order_request(built_order).await?;
66
67        let path = self.build_create_order_request_path(true)?;
68
69        let CreateOrderResponse { task_id, .. } = self.relayer_client.post(&path, request).await?;
70
71        // Create a task waiter for the task
72        let task_waiter = self.watch_task(task_id, DEFAULT_TASK_TIMEOUT).await?;
73        Ok(task_waiter)
74    }
75}
76
77// --- Private Helpers --- //
78impl RenegadeClient {
79    /// Builds the order creation request from the given built order
80    async fn build_create_order_request(
81        &self,
82        built_order: BuiltOrder,
83    ) -> Result<CreateOrderRequest, RenegadeClientError> {
84        let auth = self.build_order_auth(&built_order.order).await?;
85
86        Ok(CreateOrderRequest {
87            order: built_order.order,
88            auth,
89            precompute_cancellation_proof: built_order.precompute_cancellation_proof,
90        })
91    }
92
93    /// Builds the order authorization for the given order according to its type
94    pub(crate) async fn build_order_auth(
95        &self,
96        order: &ApiOrderCore,
97    ) -> Result<OrderAuth, RenegadeClientError> {
98        let intent = order.get_intent();
99
100        // For public orders, we only need to sign over the circuit intent & executor
101        // address
102        if matches!(order.order_type, OrderType::PublicOrder) {
103            let sol_permit =
104                PublicIntentPermit { intent: intent.into(), executor: self.get_executor_address() };
105            let chain_id = self.get_chain_id();
106            let intent_signature = sol_permit
107                .sign(chain_id, self.get_account_signer())
108                .map_err(RenegadeClientError::signing)?
109                .into();
110            let permit: ApiPublicIntentPermit = sol_permit.into();
111
112            return Ok(OrderAuth::PublicOrder { permit, intent_signature });
113        }
114
115        // For private orders, we need to sample the correct recovery & share stream
116        // seeds, then compute a commitment to the intent state object.
117        let (mut recovery_seed_csprng, mut share_seed_csprng) = self.get_account_seeds().await?;
118        let intent_recovery_stream_seed = recovery_seed_csprng.next().unwrap();
119        let intent_share_stream_seed = share_seed_csprng.next().unwrap();
120
121        match order.order_type {
122            OrderType::NativelySettledPrivateOrder => {
123                // For Ring 1, compute recovery_id first, then compute commitment.
124                // The relayer validates using the same ordering.
125                let mut state_intent = DarkpoolStateIntent::new(
126                    intent,
127                    intent_share_stream_seed,
128                    intent_recovery_stream_seed,
129                );
130                state_intent.compute_recovery_id();
131                let commitment = state_intent.compute_commitment();
132
133                // Sign the commitment with ECDSA using a nonce
134                // SignatureWithNonce::sign internally hashes the payload, so pass raw bytes
135                let commitment_u256 = scalar_to_u256(&commitment);
136                let chain_id = self.get_chain_id();
137                let sig = SignatureWithNonce::sign(
138                    &commitment_u256.to_be_bytes::<32>(),
139                    chain_id,
140                    self.get_account_signer(),
141                )
142                .map_err(RenegadeClientError::signing)?;
143                let intent_signature: ApiSignatureWithNonce = sig.into();
144                Ok(OrderAuth::NativelySettledPrivateOrder { intent_signature })
145            },
146            OrderType::RenegadeSettledPublicFillOrder
147            | OrderType::RenegadeSettledPrivateFillOrder => {
148                // For Ring 2/3, the circuit expects the signature over the *original*
149                // intent commitment (before compute_recovery_id is called).
150                let state_intent = DarkpoolStateIntent::new(
151                    intent,
152                    intent_share_stream_seed,
153                    intent_recovery_stream_seed,
154                );
155                let commitment = state_intent.compute_commitment();
156
157                // Sign the commitment with Schnorr
158                let intent_signature = self.schnorr_sign(&commitment)?.into();
159
160                // Renegade-settled orders *may* require the creation of a new output balance,
161                // which we authorize optimistically by generating a Schnorr signature over a
162                // commitment to the new balance state object.
163                let out_token = order.intent.out_token;
164                let owner = order.intent.owner;
165
166                let new_output_balance = DarkpoolBalance::new(
167                    out_token,
168                    owner,
169                    self.get_relayer_fee_recipient(),
170                    self.get_schnorr_public_key(),
171                );
172
173                let balance_recovery_stream_seed = recovery_seed_csprng.next().unwrap();
174                let balance_share_stream_seed = share_seed_csprng.next().unwrap();
175
176                let state_output_balance = DarkpoolStateBalance::new(
177                    new_output_balance,
178                    balance_share_stream_seed,
179                    balance_recovery_stream_seed,
180                );
181
182                let balance_commitment = state_output_balance.compute_commitment();
183                let new_output_balance_signature = self.schnorr_sign(&balance_commitment)?.into();
184
185                Ok(OrderAuth::RenegadeSettledOrder {
186                    intent_signature,
187                    new_output_balance_signature,
188                })
189            },
190            OrderType::PublicOrder => unreachable!(),
191        }
192    }
193
194    /// Builds the request path for the create order endpoint
195    fn build_create_order_request_path(
196        &self,
197        non_blocking: bool,
198    ) -> Result<String, RenegadeClientError> {
199        let path = construct_http_path!(CREATE_ORDER_ROUTE, "account_id" => self.get_account_id());
200        let query_string =
201            serde_urlencoded::to_string(&[(NON_BLOCKING_PARAM, non_blocking.to_string())])
202                .map_err(RenegadeClientError::serde)?;
203
204        Ok(format!("{path}?{query_string}"))
205    }
206}
207
208// -----------------
209// | Order Builder |
210// -----------------
211
212/// The result of building an order
213#[derive(Debug)]
214pub struct BuiltOrder {
215    /// The order to be placed
216    pub order: ApiOrderCore,
217    /// Whether to precompute a cancellation proof for the order
218    pub precompute_cancellation_proof: bool,
219}
220
221/// Builder for order configuration
222#[derive(Debug)]
223pub struct OrderBuilder {
224    /// The owner of the order
225    owner: Address,
226    /// The ID of the order to create. If not provided, a new UUID will be
227    /// generated.
228    id: Option<Uuid>,
229    /// The input token mint address.
230    input_mint: Option<Address>,
231    /// The output token mint address.
232    output_mint: Option<Address>,
233    /// The amount of the input token to trade.
234    amount_in: Option<Amount>,
235    /// The minimum output token amount that must be received from the order.
236    ///
237    /// This is used to compute a minimum price (in terms of output token per
238    /// input token) below which fills will not execute.
239    min_output_amount: Option<Amount>,
240    /// The minimum amount that must be filled for the order to execute.
241    min_fill_size: Option<Amount>,
242    /// The type of order to create.
243    order_type: Option<OrderType>,
244    /// Whether to allow external matches on the order
245    allow_external_matches: Option<bool>,
246    /// Whether to precompute a cancellation proof for the order.
247    precompute_cancellation_proof: Option<bool>,
248}
249
250impl OrderBuilder {
251    /// Create a new OrderBuilder with the given owner
252    pub fn new(owner: Address) -> Self {
253        Self {
254            owner,
255            id: None,
256            input_mint: None,
257            output_mint: None,
258            amount_in: None,
259            min_output_amount: None,
260            min_fill_size: None,
261            order_type: None,
262            allow_external_matches: None,
263            precompute_cancellation_proof: None,
264        }
265    }
266
267    /// Set the ID of the order to create
268    pub fn with_id(mut self, id: Uuid) -> Self {
269        self.id = Some(id);
270        self
271    }
272
273    /// Set the input mint address
274    pub fn with_input_mint(mut self, input_mint: &str) -> Result<Self, RenegadeClientError> {
275        let input_mint_address =
276            Address::from_str(input_mint).map_err(RenegadeClientError::invalid_order)?;
277
278        self.input_mint = Some(input_mint_address);
279        Ok(self)
280    }
281
282    /// Set the output mint address
283    pub fn with_output_mint(mut self, output_mint: &str) -> Result<Self, RenegadeClientError> {
284        let output_mint_address =
285            Address::from_str(output_mint).map_err(RenegadeClientError::invalid_order)?;
286
287        self.output_mint = Some(output_mint_address);
288        Ok(self)
289    }
290
291    /// Set the order input token amount
292    pub fn with_input_amount(mut self, amount: Amount) -> Self {
293        self.amount_in = Some(amount);
294        self
295    }
296
297    /// Set the minimum output token amount that must be received from the
298    /// order.
299    ///
300    /// This is used to compute a minimum price (in terms of output token per
301    /// input token) below which fills will not execute.
302    pub fn with_min_output_amount(mut self, amount: Amount) -> Self {
303        self.min_output_amount = Some(amount);
304        self
305    }
306
307    /// Set the minimum fill size
308    pub fn with_min_fill_size(mut self, min_fill: Amount) -> Self {
309        self.min_fill_size = Some(min_fill);
310        self
311    }
312
313    /// Set whether external matches are allowed
314    pub fn with_allow_external_matches(mut self, allow: bool) -> Self {
315        self.allow_external_matches = Some(allow);
316        self
317    }
318
319    /// Set the order type, i.e. which level of privacy to prescribe to it.
320    pub fn with_order_type(mut self, order_type: OrderType) -> Self {
321        self.order_type = Some(order_type);
322        self
323    }
324
325    /// Set whether to precompute a cancellation proof for the order
326    pub fn with_precompute_cancellation_proof(mut self, precompute: bool) -> Self {
327        self.precompute_cancellation_proof = Some(precompute);
328        self
329    }
330
331    /// Build the order, validating all required fields
332    pub fn build(self) -> Result<BuiltOrder, RenegadeClientError> {
333        let amount_in = unwrap_field!(self, amount_in);
334
335        let min_output_amount: FixedPoint = self.min_output_amount.unwrap_or_default().into();
336        let min_price = if min_output_amount == FixedPoint::from(0u64) {
337            FixedPoint::from(0u64)
338        } else {
339            min_output_amount.ceil_div_int(amount_in).into()
340        };
341
342        let order = ApiOrderCore {
343            id: self.id.unwrap_or_else(Uuid::new_v4),
344            intent: ApiIntent {
345                in_token: unwrap_field!(self, input_mint),
346                out_token: unwrap_field!(self, output_mint),
347                owner: self.owner,
348                amount_in,
349                min_price,
350            },
351            min_fill_size: self.min_fill_size.unwrap_or(0),
352            order_type: unwrap_field!(self, order_type),
353            allow_external_matches: self.allow_external_matches.unwrap_or(true),
354        };
355
356        let precompute_cancellation_proof = self.precompute_cancellation_proof.unwrap_or(false);
357
358        Ok(BuiltOrder { order, precompute_cancellation_proof })
359    }
360}