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#[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 MarketSwap,
30 MarketIncrease,
32 MarketDecrease,
34 LimitSwap,
36 LimitIncrease,
38 LimitDecrease,
40 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 pub fn is_swap(&self) -> bool {
61 matches!(self, Self::MarketSwap | Self::LimitSwap)
62 }
63
64 pub fn is_increase(&self) -> bool {
66 matches!(self, Self::MarketIncrease | Self::LimitIncrease)
67 }
68
69 pub fn is_decrease(&self) -> bool {
71 matches!(
72 self,
73 Self::MarketDecrease | Self::LimitDecrease | Self::StopLossDecrease
74 )
75 }
76}
77
78#[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 NoSwap,
86 PnlTokenToCollateralToken,
88 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#[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 #[builder(setter(into))]
124 pub market_token: StringPubkey,
125 pub is_long: bool,
127 pub size: u128,
129 #[cfg_attr(serde, serde(default))]
133 #[builder(default)]
134 pub amount: u128,
135 #[cfg_attr(serde, serde(default))]
141 #[builder(default)]
142 pub min_output: u128,
143 #[cfg_attr(serde, serde(default))]
145 #[builder(default, setter(strip_option))]
146 pub trigger_price: Option<u128>,
147 #[cfg_attr(serde, serde(default))]
149 #[builder(default, setter(strip_option))]
150 pub acceptable_price: Option<u128>,
151 #[cfg_attr(serde, serde(default))]
153 #[builder(default, setter(strip_option))]
154 pub decrease_position_swap_type: Option<DecreasePositionSwapType>,
155 #[cfg_attr(serde, serde(default))]
157 #[builder(default, setter(strip_option))]
158 pub valid_from_ts: Option<i64>,
159}
160
161#[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 #[cfg_attr(serde, serde(default))]
169 #[builder(default)]
170 pub program: StoreProgram,
171 #[builder(setter(into))]
173 pub payer: StringPubkey,
174 #[cfg_attr(serde, serde(default))]
176 #[builder(default, setter(strip_option, into))]
177 pub receiver: Option<StringPubkey>,
178 #[cfg_attr(serde, serde(default))]
180 #[builder(default, setter(strip_option, into))]
181 pub nonce: Option<NonceBytes>,
182 #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
184 #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_ORDER)]
185 pub execution_lamports: u64,
186 pub kind: CreateOrderKind,
188 #[builder(setter(into))]
190 pub collateral_or_swap_out_token: StringPubkey,
191 pub params: CreateOrderParams,
193 #[cfg_attr(serde, serde(default))]
195 #[builder(default, setter(into))]
196 pub pay_token: Option<StringPubkey>,
197 #[cfg_attr(serde, serde(default))]
199 #[builder(default, setter(into))]
200 pub pay_token_account: Option<StringPubkey>,
201 #[cfg_attr(serde, serde(default))]
203 #[builder(default, setter(into))]
204 pub receive_token: Option<StringPubkey>,
205 #[cfg_attr(serde, serde(default))]
207 #[builder(default, setter(into))]
208 pub swap_path: Vec<StringPubkey>,
209 #[cfg_attr(serde, serde(default))]
211 #[builder(default)]
212 pub unwrap_native_on_receive: bool,
213 #[cfg_attr(serde, serde(default))]
215 #[builder(default)]
216 pub callback: Option<Callback>,
217 #[cfg_attr(serde, serde(default))]
219 #[builder(default)]
220 pub skip_position_creation: bool,
221 #[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#[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 #[builder(setter(into))]
250 pub long_token: StringPubkey,
251 #[builder(setter(into))]
253 pub short_token: StringPubkey,
254}
255
256impl CreateOrderHint {
257 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}