Skip to main content

gmsol_sdk/builders/order/
create.rs

1use anchor_spl::associated_token::{self, get_associated_token_address_with_program_id};
2use gmsol_programs::gmsol_store::client::args;
3use gmsol_programs::gmsol_store::types::CreateOrderParams as StoreCreateOrderParams;
4use gmsol_programs::gmsol_store::{client::accounts, types::OrderKind};
5use gmsol_solana_utils::client_traits::FromRpcClientWith;
6use gmsol_solana_utils::ProgramExt;
7use solana_sdk::system_program;
8use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
9use typed_builder::TypedBuilder;
10
11use crate::builders::callback::{Callback, CallbackParams};
12use crate::builders::{
13    utils::{generate_nonce, prepare_ata},
14    NonceBytes, StoreProgram,
15};
16use crate::builders::{MarketTokenIxBuilder, PoolTokenHint, StoreProgramIxBuilder};
17use crate::serde::StringPubkey;
18use crate::{AtomicGroup, IntoAtomicGroup};
19
20use super::{PreparePosition, MIN_EXECUTION_LAMPORTS_FOR_ORDER};
21
22/// Create Order Kind.
23#[cfg_attr(js, derive(tsify_next::Tsify))]
24#[cfg_attr(js, tsify(from_wasm_abi))]
25#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
26#[derive(Debug, Clone, Copy)]
27pub enum CreateOrderKind {
28    /// Market Swap.
29    MarketSwap,
30    /// Market Increase.
31    MarketIncrease,
32    /// Market Decrease.
33    MarketDecrease,
34    /// Limit Swap.
35    LimitSwap,
36    /// Limit Increase.
37    LimitIncrease,
38    /// Limit Decrease.
39    LimitDecrease,
40    /// Stop-loss Decrease.
41    StopLossDecrease,
42}
43
44impl From<CreateOrderKind> for OrderKind {
45    fn from(kind: CreateOrderKind) -> Self {
46        match kind {
47            CreateOrderKind::MarketSwap => Self::MarketSwap,
48            CreateOrderKind::MarketIncrease => Self::MarketIncrease,
49            CreateOrderKind::MarketDecrease => Self::MarketDecrease,
50            CreateOrderKind::LimitSwap => Self::LimitSwap,
51            CreateOrderKind::LimitIncrease => Self::LimitIncrease,
52            CreateOrderKind::LimitDecrease => Self::LimitDecrease,
53            CreateOrderKind::StopLossDecrease => Self::StopLossDecrease,
54        }
55    }
56}
57
58impl CreateOrderKind {
59    /// Returns whether the order kind is "swap".
60    pub fn is_swap(&self) -> bool {
61        matches!(self, Self::MarketSwap | Self::LimitSwap)
62    }
63
64    /// Returns whether the order kind is "increase".
65    pub fn is_increase(&self) -> bool {
66        matches!(self, Self::MarketIncrease | Self::LimitIncrease)
67    }
68
69    /// Returns whether the ordr kind is "decrease".
70    pub fn is_decrease(&self) -> bool {
71        matches!(
72            self,
73            Self::MarketDecrease | Self::LimitDecrease | Self::StopLossDecrease
74        )
75    }
76}
77
78/// Swap type for decreasing position.
79#[cfg_attr(js, derive(tsify_next::Tsify))]
80#[cfg_attr(js, tsify(from_wasm_abi))]
81#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
82#[derive(Debug, Clone, Copy)]
83pub enum DecreasePositionSwapType {
84    /// Do not swap.
85    NoSwap,
86    /// Swap PnL token to collateral token.
87    PnlTokenToCollateralToken,
88    /// Swap collateral token to PnL token.
89    CollateralToPnlToken,
90}
91
92impl From<DecreasePositionSwapType>
93    for gmsol_programs::gmsol_store::types::DecreasePositionSwapType
94{
95    fn from(ty: DecreasePositionSwapType) -> Self {
96        match ty {
97            DecreasePositionSwapType::NoSwap => Self::NoSwap,
98            DecreasePositionSwapType::PnlTokenToCollateralToken => Self::PnlTokenToCollateralToken,
99            DecreasePositionSwapType::CollateralToPnlToken => Self::CollateralToPnlToken,
100        }
101    }
102}
103
104impl From<DecreasePositionSwapType>
105    for gmsol_model::action::decrease_position::DecreasePositionSwapType
106{
107    fn from(ty: DecreasePositionSwapType) -> Self {
108        match ty {
109            DecreasePositionSwapType::NoSwap => Self::NoSwap,
110            DecreasePositionSwapType::PnlTokenToCollateralToken => Self::PnlTokenToCollateralToken,
111            DecreasePositionSwapType::CollateralToPnlToken => Self::CollateralToPnlToken,
112        }
113    }
114}
115
116/// Parameters for creating an order.
117#[cfg_attr(js, derive(tsify_next::Tsify))]
118#[cfg_attr(js, tsify(from_wasm_abi))]
119#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
120#[derive(Debug, Clone, TypedBuilder)]
121pub struct CreateOrderParams {
122    /// The market token of the market in which the order will be created.
123    #[builder(setter(into))]
124    pub market_token: StringPubkey,
125    /// Whether the order is for a long or short position.
126    pub is_long: bool,
127    /// Delta size in USD.
128    pub size: u128,
129    /// Delta amount of tokens:
130    /// - For increase / swap orders, it is the amount of pay tokens.
131    /// - For decrease orders, it is the amount of collateral tokens to withdraw.
132    #[cfg_attr(serde, serde(default))]
133    #[builder(default)]
134    pub amount: u128,
135    /// Minimum amount or value of output tokens.
136    ///
137    /// - Minimum collateral amount for increase-position orders after swap.
138    /// - Minimum swap-out amount for swap orders.
139    /// - Minimum output value for decrease-position orders.
140    #[cfg_attr(serde, serde(default))]
141    #[builder(default)]
142    pub min_output: u128,
143    /// Trigger price (in unit price).
144    #[cfg_attr(serde, serde(default))]
145    #[builder(default, setter(strip_option))]
146    pub trigger_price: Option<u128>,
147    /// Acceptable price (in unit price).
148    #[cfg_attr(serde, serde(default))]
149    #[builder(default, setter(strip_option))]
150    pub acceptable_price: Option<u128>,
151    /// Decrease Position Swap Type.
152    #[cfg_attr(serde, serde(default))]
153    #[builder(default, setter(strip_option))]
154    pub decrease_position_swap_type: Option<DecreasePositionSwapType>,
155    /// Timestamp from which the order becomes valid.
156    #[cfg_attr(serde, serde(default))]
157    #[builder(default, setter(strip_option))]
158    pub valid_from_ts: Option<i64>,
159}
160
161/// Builder for the `create_order` instruction.
162#[cfg_attr(js, derive(tsify_next::Tsify))]
163#[cfg_attr(js, tsify(from_wasm_abi))]
164#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
165#[derive(Debug, Clone, TypedBuilder)]
166pub struct CreateOrder {
167    /// Program.
168    #[cfg_attr(serde, serde(default))]
169    #[builder(default)]
170    pub program: StoreProgram,
171    /// Payer (a.k.a. owner).
172    #[builder(setter(into))]
173    pub payer: StringPubkey,
174    /// Reciever.
175    #[cfg_attr(serde, serde(default))]
176    #[builder(default, setter(strip_option, into))]
177    pub receiver: Option<StringPubkey>,
178    /// Nonce for the order.
179    #[cfg_attr(serde, serde(default))]
180    #[builder(default, setter(strip_option, into))]
181    pub nonce: Option<NonceBytes>,
182    /// Execution fee paid to the keeper in lamports.
183    #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
184    #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_ORDER)]
185    pub execution_lamports: u64,
186    /// Order Kind.
187    pub kind: CreateOrderKind,
188    /// Collateral or swap out token.
189    #[builder(setter(into))]
190    pub collateral_or_swap_out_token: StringPubkey,
191    /// Order Parameters.
192    pub params: CreateOrderParams,
193    /// Pay token.
194    #[cfg_attr(serde, serde(default))]
195    #[builder(default, setter(into))]
196    pub pay_token: Option<StringPubkey>,
197    /// Pay token account.
198    #[cfg_attr(serde, serde(default))]
199    #[builder(default, setter(into))]
200    pub pay_token_account: Option<StringPubkey>,
201    /// Receive token.
202    #[cfg_attr(serde, serde(default))]
203    #[builder(default, setter(into))]
204    pub receive_token: Option<StringPubkey>,
205    /// Swap path.
206    #[cfg_attr(serde, serde(default))]
207    #[builder(default, setter(into))]
208    pub swap_path: Vec<StringPubkey>,
209    /// Whether to unwrap the native token when receiving (e.g., convert WSOL to SOL).
210    #[cfg_attr(serde, serde(default))]
211    #[builder(default)]
212    pub unwrap_native_on_receive: bool,
213    /// Callback.
214    #[cfg_attr(serde, serde(default))]
215    #[builder(default)]
216    pub callback: Option<Callback>,
217    /// Whether to skip position account creation.
218    #[cfg_attr(serde, serde(default))]
219    #[builder(default)]
220    pub skip_position_creation: bool,
221    /// Whether to force position account creation.
222    #[cfg_attr(serde, serde(default))]
223    #[builder(default)]
224    pub force_position_creation: bool,
225}
226
227#[cfg(serde)]
228fn default_execution_lamports() -> u64 {
229    MIN_EXECUTION_LAMPORTS_FOR_ORDER
230}
231
232impl CreateOrder {
233    fn is_collateral_or_swap_out_token_long(
234        &self,
235        hint: &CreateOrderHint,
236    ) -> Result<bool, crate::SolanaUtilsError> {
237        let token = &*self.collateral_or_swap_out_token;
238        hint.is_collateral_long(token)
239    }
240}
241
242/// Hint for [`CreateOrder`].
243#[cfg_attr(js, derive(tsify_next::Tsify))]
244#[cfg_attr(js, tsify(from_wasm_abi))]
245#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
246#[derive(Debug, Clone, TypedBuilder)]
247pub struct CreateOrderHint {
248    /// Long token.
249    #[builder(setter(into))]
250    pub long_token: StringPubkey,
251    /// Short token.
252    #[builder(setter(into))]
253    pub short_token: StringPubkey,
254}
255
256impl CreateOrderHint {
257    /// Returns whether the given token is long token or short token.
258    /// # Errors
259    /// - Returns Error if the given `collateral` is not one of the specified long or short tokens.
260    pub fn is_collateral_long(&self, collateral: &Pubkey) -> Result<bool, crate::SolanaUtilsError> {
261        PoolTokenHint {
262            long_token: self.long_token,
263            short_token: self.short_token,
264        }
265        .is_collateral_long(collateral)
266    }
267}
268
269impl IntoAtomicGroup for CreateOrder {
270    type Hint = CreateOrderHint;
271
272    fn into_atomic_group(self, hint: &Self::Hint) -> Result<AtomicGroup, crate::SolanaUtilsError> {
273        let owner = self.payer.0;
274
275        let mut insts = AtomicGroup::new(&owner);
276
277        let receiver = self.receiver.as_deref().copied().unwrap_or(owner);
278        let nonce = self.nonce.unwrap_or_else(generate_nonce);
279        let order = self.program.find_order_address(&owner, &nonce);
280        let token_program_id = anchor_spl::token::ID;
281
282        let collateral_or_swap_out_token = self.collateral_or_swap_out_token.0;
283        let is_collateral_or_swap_out_token_long =
284            self.is_collateral_or_swap_out_token_long(hint)?;
285
286        let (pay_token, receive_token, long_token, short_token, is_position_order) = match self.kind
287        {
288            CreateOrderKind::MarketSwap | CreateOrderKind::LimitSwap => {
289                if let Some(receive_token) = self.receive_token {
290                    if receive_token.0 != collateral_or_swap_out_token {
291                        return Err(crate::SolanaUtilsError::custom("invalid `receive_token`: it must be the same as `collateral_or_swap_out_token` for swap orders if provided"));
292                    }
293                }
294                (
295                    Some(
296                        self.pay_token
297                            .as_deref()
298                            .copied()
299                            .unwrap_or(collateral_or_swap_out_token),
300                    ),
301                    Some(collateral_or_swap_out_token),
302                    None,
303                    None,
304                    false,
305                )
306            }
307            CreateOrderKind::MarketIncrease | CreateOrderKind::LimitIncrease => (
308                Some(
309                    self.pay_token
310                        .as_deref()
311                        .copied()
312                        .unwrap_or(collateral_or_swap_out_token),
313                ),
314                None,
315                Some(hint.long_token.0),
316                Some(hint.short_token.0),
317                true,
318            ),
319            CreateOrderKind::MarketDecrease
320            | CreateOrderKind::LimitDecrease
321            | CreateOrderKind::StopLossDecrease => (
322                None,
323                Some(
324                    self.receive_token
325                        .as_deref()
326                        .copied()
327                        .unwrap_or(collateral_or_swap_out_token),
328                ),
329                Some(hint.long_token.0),
330                Some(hint.short_token.0),
331                true,
332            ),
333        };
334
335        let pay_token_account = pay_token.as_ref().map(|token| {
336            self.pay_token_account
337                .as_deref()
338                .copied()
339                .unwrap_or_else(|| {
340                    get_associated_token_address_with_program_id(&owner, token, &token_program_id)
341                })
342        });
343        let (pay_token_escrow, prepare) =
344            prepare_ata(&owner, &order, pay_token.as_ref(), &token_program_id).unzip();
345        insts.extend(prepare);
346        let (receive_token_escrow, prepare) =
347            prepare_ata(&owner, &order, receive_token.as_ref(), &token_program_id).unzip();
348        insts.extend(prepare);
349        let (long_token_escrow, prepare) =
350            prepare_ata(&owner, &order, long_token.as_ref(), &token_program_id).unzip();
351        insts.extend(prepare);
352        let (short_token_escrow, prepare) =
353            prepare_ata(&owner, &order, short_token.as_ref(), &token_program_id).unzip();
354        insts.extend(prepare);
355
356        let market = self.program.find_market_address(&self.params.market_token);
357        let user = self.program.find_user_address(&owner);
358        let position = (is_position_order).then(|| {
359            self.program.find_position_address(
360                &owner,
361                &self.params.market_token,
362                &collateral_or_swap_out_token,
363                self.params.is_long,
364            )
365        });
366        let params = &self.params;
367        let swap_markets = self
368            .swap_path
369            .iter()
370            .map(|mint| AccountMeta {
371                pubkey: self.program.find_market_address(mint),
372                is_signer: false,
373                is_writable: false,
374            })
375            .collect::<Vec<_>>();
376
377        let params = StoreCreateOrderParams {
378            kind: self.kind.into(),
379            decrease_position_swap_type: params.decrease_position_swap_type.map(Into::into),
380            execution_lamports: self.execution_lamports,
381            swap_path_length: self.swap_path.len() as u8,
382            initial_collateral_delta_amount: self
383                .params
384                .amount
385                .try_into()
386                .map_err(crate::SolanaUtilsError::custom)?,
387            size_delta_value: self.params.size,
388            is_long: self.params.is_long,
389            is_collateral_long: is_collateral_or_swap_out_token_long,
390            min_output: Some(self.params.min_output),
391            trigger_price: self.params.trigger_price,
392            acceptable_price: self.params.acceptable_price,
393            should_unwrap_native_token: self.unwrap_native_on_receive,
394            valid_from_ts: self.params.valid_from_ts,
395        };
396
397        if is_position_order && self.skip_position_creation && self.force_position_creation {
398            return Err(crate::SolanaUtilsError::custom(
399                "invalid parameters: `skip_position_creation` and `force_position_creation` are both set",
400            ));
401        }
402
403        if (is_position_order && !self.skip_position_creation)
404            && (self.kind.is_increase() || self.force_position_creation)
405        {
406            let prepare = PreparePosition::builder()
407                .program(self.program.clone())
408                .collateral_token(collateral_or_swap_out_token)
409                .execution_lamports(self.execution_lamports)
410                .kind(self.kind)
411                .params(self.params.clone())
412                .payer(self.payer)
413                .should_unwrap_native_token(params.should_unwrap_native_token)
414                .swap_path_length(params.swap_path_length)
415                .build()
416                .into_atomic_group(hint)?;
417            insts.merge(prepare);
418        }
419
420        let CallbackParams {
421            callback_version,
422            callback_authority,
423            callback_program,
424            callback_shared_data_account,
425            callback_partitioned_data_account,
426        } = self.program.get_callback_params(self.callback.as_ref());
427
428        let create = self
429            .program
430            .anchor_instruction(args::CreateOrderV2 {
431                nonce: nonce.to_bytes(),
432                params,
433                callback_version,
434            })
435            .anchor_accounts(
436                accounts::CreateOrderV2 {
437                    owner,
438                    receiver,
439                    store: self.program.store.0,
440                    market,
441                    user,
442                    order,
443                    position,
444                    initial_collateral_token: pay_token,
445                    final_output_token: receive_token.unwrap_or(collateral_or_swap_out_token),
446                    long_token,
447                    short_token,
448                    initial_collateral_token_escrow: pay_token_escrow,
449                    final_output_token_escrow: receive_token_escrow,
450                    long_token_escrow,
451                    short_token_escrow,
452                    initial_collateral_token_source: pay_token_account,
453                    system_program: system_program::ID,
454                    token_program: token_program_id,
455                    associated_token_program: associated_token::ID,
456                    event_authority: self.program.find_event_authority_address(),
457                    program: self.program.id.0,
458                    callback_authority,
459                    callback_program,
460                    callback_shared_data_account,
461                    callback_partitioned_data_account,
462                },
463                true,
464            )
465            .anchor_accounts(swap_markets, false)
466            .build();
467
468        insts.add(create);
469
470        Ok(insts)
471    }
472}
473
474impl StoreProgramIxBuilder for CreateOrder {
475    fn store_program(&self) -> &StoreProgram {
476        &self.program
477    }
478}
479
480impl MarketTokenIxBuilder for CreateOrder {
481    fn market_token(&self) -> &Pubkey {
482        &self.params.market_token
483    }
484}
485
486impl FromRpcClientWith<CreateOrder> for CreateOrderHint {
487    async fn from_rpc_client_with<'a>(
488        builder: &'a CreateOrder,
489        client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
490    ) -> gmsol_solana_utils::Result<Self> {
491        let hint = PoolTokenHint::from_rpc_client_with(builder, client).await?;
492        Ok(Self {
493            long_token: hint.long_token,
494            short_token: hint.short_token,
495        })
496    }
497}
498
499#[cfg(test)]
500mod tests {
501
502    #[cfg(not(target_arch = "wasm32"))]
503    use tokio::test as async_test;
504
505    #[cfg(target_arch = "wasm32")]
506    use wasm_bindgen_test::wasm_bindgen_test as async_test;
507
508    use gmsol_solana_utils::{
509        client_traits::GenericRpcClient, cluster::Cluster, transaction_builder::default_before_sign,
510    };
511    use solana_sdk::pubkey::Pubkey;
512
513    use super::*;
514
515    #[test]
516    fn create_order() -> crate::Result<()> {
517        let long_token = Pubkey::new_unique();
518        let short_token = Pubkey::new_unique();
519        let params = CreateOrderParams::builder()
520            .market_token(Pubkey::new_unique())
521            .is_long(true)
522            .size(1_000 * crate::constants::MARKET_USD_UNIT)
523            .build();
524        CreateOrder::builder()
525            .payer(Pubkey::new_unique())
526            .kind(CreateOrderKind::MarketIncrease)
527            .collateral_or_swap_out_token(long_token)
528            .params(params)
529            .swap_path([Pubkey::new_unique().into()])
530            .build()
531            .into_atomic_group(
532                &CreateOrderHint::builder()
533                    .long_token(long_token)
534                    .short_token(short_token)
535                    .build(),
536            )?
537            .partially_signed_transaction_with_blockhash_and_options(
538                Default::default(),
539                Default::default(),
540                None,
541                default_before_sign,
542            )?;
543        Ok(())
544    }
545
546    #[async_test]
547    async fn create_order_with_rpc() -> crate::Result<()> {
548        let market_token: Pubkey = "5sdFW7wrKsxxYHMXoqDmNHkGyCWsbLEFb1x1gzBBm4Hx".parse()?;
549        let wsol: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
550
551        let cluster = Cluster::Devnet;
552        let client = GenericRpcClient::new(cluster.url());
553
554        let params = CreateOrderParams::builder()
555            .market_token(market_token)
556            .is_long(true)
557            .size(1_000 * crate::constants::MARKET_USD_UNIT)
558            .build();
559        CreateOrder::builder()
560            .payer(Pubkey::new_unique())
561            .kind(CreateOrderKind::MarketIncrease)
562            .collateral_or_swap_out_token(wsol)
563            .params(params)
564            .swap_path([Pubkey::new_unique().into()])
565            .build()
566            .into_atomic_group_with_rpc_client(&client)
567            .await?
568            .partially_signed_transaction_with_blockhash_and_options(
569                Default::default(),
570                Default::default(),
571                None,
572                default_before_sign,
573            )?;
574
575        Ok(())
576    }
577}