1use std::{collections::HashMap, ops::Deref, sync::Arc};
2
3use anchor_client::{
4 anchor_lang::{system_program, Id},
5 solana_sdk::{address_lookup_table::AddressLookupTableAccount, pubkey::Pubkey, signer::Signer},
6};
7use anchor_spl::associated_token::get_associated_token_address;
8use gmsol_solana_utils::{
9 bundle_builder::{BundleBuilder, BundleOptions},
10 compute_budget::ComputeBudget,
11};
12use gmsol_store::{
13 accounts, instruction,
14 ops::order::PositionCutKind,
15 states::{
16 common::TokensWithFeed, user::UserHeader, MarketMeta, NonceBytes, Position,
17 PriceProviderKind, Pyth, Store, TokenMap,
18 },
19};
20
21use crate::{
22 exchange::generate_nonce,
23 store::{token::TokenAccountOps, utils::FeedsParser},
24 utils::{
25 builder::{
26 FeedAddressMap, FeedIds, MakeBundleBuilder, PullOraclePriceConsumer, SetExecutionFee,
27 },
28 fix_optional_account_metas, ZeroCopy,
29 },
30};
31
32use super::{
33 order::{recent_timestamp, ClaimableAccountsBuilder, CloseOrderHint},
34 ExchangeOps,
35};
36
37pub const POSITION_CUT_COMPUTE_BUDGET: u32 = 400_000;
39
40pub struct PositionCutBuilder<'a, C> {
42 client: &'a crate::Client<C>,
43 kind: PositionCutKind,
44 nonce: Option<NonceBytes>,
45 recent_timestamp: i64,
46 execution_fee: u64,
47 oracle: Pubkey,
48 position: Pubkey,
49 price_provider: Pubkey,
50 hint: Option<PositionCutHint>,
51 feeds_parser: FeedsParser,
52 close: bool,
53 event_buffer_index: u16,
54 alts: HashMap<Pubkey, Vec<Pubkey>>,
55}
56
57#[derive(Clone)]
59pub struct PositionCutHint {
60 tokens_with_feed: TokensWithFeed,
61 meta: MarketMeta,
62 store_address: Pubkey,
63 owner: Pubkey,
64 user: Pubkey,
65 referrer: Option<Pubkey>,
66 store: Arc<Store>,
67 collateral_token: Pubkey,
68 pnl_token: Pubkey,
69 token_map: Pubkey,
70 market: Pubkey,
71 position_size: u128,
72}
73
74impl PositionCutHint {
75 pub async fn from_position<C: Deref<Target = impl Signer> + Clone>(
77 client: &crate::Client<C>,
78 position: &Position,
79 ) -> crate::Result<Self> {
80 let store_address = position.store;
81 let store = client.store(&store_address).await?;
82 let token_map_address = client
83 .authorized_token_map_address(&store_address)
84 .await?
85 .ok_or(crate::Error::invalid_argument(
86 "token map is not configurated for the store",
87 ))?;
88 let token_map = client.token_map(&token_map_address).await?;
89 let market = client.find_market_address(&store_address, &position.market_token);
90 let meta = *client.market(&market).await?.meta();
91 let user = client.find_user_address(&store_address, &position.owner);
92 let user = client
93 .account::<ZeroCopy<UserHeader>>(&user)
94 .await?
95 .map(|user| user.0);
96
97 Self::try_new(
98 position,
99 store,
100 &token_map,
101 market,
102 meta,
103 user.as_ref(),
104 client.store_program_id(),
105 )
106 }
107
108 pub fn try_new(
110 position: &Position,
111 store: Arc<Store>,
112 token_map: &TokenMap,
113 market: Pubkey,
114 market_meta: MarketMeta,
115 user: Option<&UserHeader>,
116 program_id: &Pubkey,
117 ) -> crate::Result<Self> {
118 use gmsol_store::states::common::token_with_feeds::token_records;
119
120 let records = token_records(
121 token_map,
122 &[
123 market_meta.index_token_mint,
124 market_meta.long_token_mint,
125 market_meta.short_token_mint,
126 ]
127 .into(),
128 )?;
129 let tokens_with_feed = TokensWithFeed::try_from_records(records)?;
130 let user_address =
131 crate::pda::find_user_pda(&position.store, &position.owner, program_id).0;
132 let referrer = user.and_then(|user| user.referral().referrer().copied());
133
134 Ok(Self {
135 store_address: position.store,
136 owner: position.owner,
137 user: user_address,
138 referrer,
139 token_map: *store.token_map().ok_or(crate::Error::invalid_argument(
140 "missing token map for the store",
141 ))?,
142 market,
143 store,
144 tokens_with_feed,
145 collateral_token: position.collateral_token,
146 pnl_token: market_meta.pnl_token(position.try_is_long()?),
147 meta: market_meta,
148 position_size: position.state.size_in_usd,
149 })
150 }
151
152 pub fn feeds(&self) -> &TokensWithFeed {
154 &self.tokens_with_feed
155 }
156}
157
158impl<'a, C: Deref<Target = impl Signer> + Clone> PositionCutBuilder<'a, C> {
159 pub(super) fn try_new(
160 client: &'a crate::Client<C>,
161 kind: PositionCutKind,
162 oracle: &Pubkey,
163 position: &Pubkey,
164 ) -> crate::Result<Self> {
165 Ok(Self {
166 client,
167 kind,
168 oracle: *oracle,
169 nonce: None,
170 recent_timestamp: recent_timestamp()?,
171 execution_fee: 0,
172 position: *position,
173 price_provider: Pyth::id(),
174 hint: None,
175 feeds_parser: Default::default(),
176 close: true,
177 event_buffer_index: 0,
178 alts: Default::default(),
179 })
180 }
181
182 pub async fn prepare_hint(&mut self) -> crate::Result<PositionCutHint> {
184 match &self.hint {
185 Some(hint) => Ok(hint.clone()),
186 None => {
187 let position = self.client.position(&self.position).await?;
188 let hint = PositionCutHint::from_position(self.client, &position).await?;
189 self.hint = Some(hint.clone());
190 Ok(hint)
191 }
192 }
193 }
194
195 pub fn close(&mut self, close: bool) -> &mut Self {
197 self.close = close;
198 self
199 }
200
201 pub fn event_buffer_index(&mut self, index: u16) -> &mut Self {
203 self.event_buffer_index = index;
204 self
205 }
206
207 pub fn hint(&mut self, hint: PositionCutHint) -> &mut Self {
209 self.hint = Some(hint);
210 self
211 }
212
213 pub fn price_provider(&mut self, program: &Pubkey) -> &mut Self {
215 self.price_provider = *program;
216 self
217 }
218
219 pub fn add_alt(&mut self, account: AddressLookupTableAccount) -> &mut Self {
221 self.alts.insert(account.key, account.addresses);
222 self
223 }
224
225 async fn build_txns(&mut self, options: BundleOptions) -> crate::Result<BundleBuilder<'a, C>> {
226 let token_program_id = anchor_spl::token::ID;
227
228 let payer = self.client.payer();
229 let nonce = self.nonce.unwrap_or_else(generate_nonce);
230 let hint = self.prepare_hint().await?;
231 let owner = hint.owner;
232 let store = hint.store_address;
233 let meta = &hint.meta;
234 let long_token_mint = meta.long_token_mint;
235 let short_token_mint = meta.short_token_mint;
236
237 let time_key = hint.store.claimable_time_key(self.recent_timestamp)?;
238 let claimable_long_token_account_for_user =
239 self.client
240 .find_claimable_account_address(&store, &long_token_mint, &owner, &time_key);
241 let claimable_short_token_account_for_user = self.client.find_claimable_account_address(
242 &store,
243 &short_token_mint,
244 &owner,
245 &time_key,
246 );
247 let claimable_pnl_token_account_for_holding = self.client.find_claimable_account_address(
248 &store,
249 &hint.pnl_token,
250 hint.store.holding(),
251 &time_key,
252 );
253 let feeds = self.feeds_parser.parse_and_sort_by_tokens(hint.feeds())?;
254 let order = self.client.find_order_address(&store, &payer, &nonce);
255
256 let long_token_escrow = get_associated_token_address(&order, &long_token_mint);
257 let short_token_escrow = get_associated_token_address(&order, &short_token_mint);
258 let output_token_escrow = get_associated_token_address(&order, &hint.collateral_token);
259 let long_token_vault = self
260 .client
261 .find_market_vault_address(&store, &long_token_mint);
262 let short_token_vault = self
263 .client
264 .find_market_vault_address(&store, &short_token_mint);
265 let event =
266 self.client
267 .find_trade_event_buffer_address(&store, &payer, self.event_buffer_index);
268
269 let prepare = self
270 .client
271 .prepare_associated_token_account(
272 &hint.collateral_token,
273 &token_program_id,
274 Some(&order),
275 )
276 .merge(self.client.prepare_associated_token_account(
277 &long_token_mint,
278 &token_program_id,
279 Some(&order),
280 ))
281 .merge(self.client.prepare_associated_token_account(
282 &short_token_mint,
283 &token_program_id,
284 Some(&order),
285 ));
286 let prepare_event_buffer = self
287 .client
288 .store_transaction()
289 .anchor_accounts(accounts::PrepareTradeEventBuffer {
290 authority: payer,
291 store,
292 event,
293 system_program: system_program::ID,
294 })
295 .anchor_args(instruction::PrepareTradeEventBuffer {
296 index: self.event_buffer_index,
297 });
298 let mut exec_builder = self
299 .client
300 .store_transaction()
301 .accounts(fix_optional_account_metas(
302 accounts::PositionCut {
303 authority: payer,
304 owner,
305 user: hint.user,
306 store,
307 token_map: hint.token_map,
308 oracle: self.oracle,
309 market: hint.market,
310 order,
311 position: self.position,
312 event,
313 long_token: long_token_mint,
314 short_token: short_token_mint,
315 long_token_escrow,
316 short_token_escrow,
317 long_token_vault,
318 short_token_vault,
319 claimable_long_token_account_for_user,
320 claimable_short_token_account_for_user,
321 claimable_pnl_token_account_for_holding,
322 system_program: system_program::ID,
323 token_program: anchor_spl::token::ID,
324 associated_token_program: anchor_spl::associated_token::ID,
325 event_authority: self.client.store_event_authority(),
326 program: *self.client.store_program_id(),
327 chainlink_program: None,
328 },
329 &crate::program_ids::DEFAULT_GMSOL_STORE_ID,
330 self.client.store_program_id(),
331 ))
332 .accounts(feeds)
333 .compute_budget(ComputeBudget::default().with_limit(POSITION_CUT_COMPUTE_BUDGET))
334 .lookup_tables(self.alts.clone());
335
336 match self.kind {
337 PositionCutKind::Liquidate => {
338 exec_builder = exec_builder.anchor_args(instruction::Liquidate {
339 nonce,
340 recent_timestamp: self.recent_timestamp,
341 execution_fee: self.execution_fee,
342 });
343 }
344 PositionCutKind::AutoDeleverage(size_delta_in_usd) => {
345 exec_builder = exec_builder.anchor_args(instruction::AutoDeleverage {
346 nonce,
347 recent_timestamp: self.recent_timestamp,
348 size_delta_in_usd,
349 execution_fee: self.execution_fee,
350 })
351 }
352 }
353
354 let is_full_close = match self.kind {
355 PositionCutKind::Liquidate => true,
356 PositionCutKind::AutoDeleverage(size) => size >= hint.position_size,
357 };
358
359 if self.close {
360 let close = self
361 .client
362 .close_order(&order)?
363 .hint(CloseOrderHint {
364 owner,
365 receiver: owner,
366 store,
367 initial_collateral_token_and_account: None,
368 final_output_token_and_account: Some((
369 hint.collateral_token,
370 output_token_escrow,
371 )),
372 long_token_and_account: Some((long_token_mint, long_token_escrow)),
373 short_token_and_account: Some((short_token_mint, short_token_escrow)),
374 user: hint.user,
375 referrer: hint.referrer,
376 rent_receiver: if is_full_close { owner } else { payer },
377 should_unwrap_native_token: true,
378 })
379 .reason("position cut")
380 .build()
381 .await?;
382 exec_builder = exec_builder.merge(close);
383 }
384
385 let (pre_builder, post_builder) = ClaimableAccountsBuilder::new(
386 self.recent_timestamp,
387 store,
388 owner,
389 *hint.store.holding(),
390 )
391 .claimable_long_token_account_for_user(
392 &long_token_mint,
393 &claimable_long_token_account_for_user,
394 )
395 .claimable_short_token_account_for_user(
396 &short_token_mint,
397 &claimable_short_token_account_for_user,
398 )
399 .claimable_pnl_token_account_for_holding(
400 &hint.pnl_token,
401 &claimable_pnl_token_account_for_holding,
402 )
403 .build(self.client);
404
405 let mut bundle = self.client.bundle_with_options(options);
406 bundle
407 .try_push(pre_builder.merge(prepare_event_buffer))?
408 .try_push(prepare.merge(exec_builder))?
409 .try_push(post_builder)?;
410 Ok(bundle)
411 }
412}
413
414impl<'a, C: Deref<Target = impl Signer> + Clone> MakeBundleBuilder<'a, C>
415 for PositionCutBuilder<'a, C>
416{
417 async fn build_with_options(
418 &mut self,
419 options: BundleOptions,
420 ) -> gmsol_solana_utils::Result<BundleBuilder<'a, C>> {
421 self.build_txns(options)
422 .await
423 .map_err(gmsol_solana_utils::Error::custom)
424 }
425}
426
427impl<C: Deref<Target = impl Signer> + Clone> PullOraclePriceConsumer for PositionCutBuilder<'_, C> {
428 async fn feed_ids(&mut self) -> crate::Result<FeedIds> {
429 let hint = self.prepare_hint().await?;
430 Ok(FeedIds::new(hint.store_address, hint.tokens_with_feed))
431 }
432
433 fn process_feeds(
434 &mut self,
435 provider: PriceProviderKind,
436 map: FeedAddressMap,
437 ) -> crate::Result<()> {
438 self.feeds_parser
439 .insert_pull_oracle_feed_parser(provider, map);
440 Ok(())
441 }
442}
443
444impl<C> SetExecutionFee for PositionCutBuilder<'_, C> {
445 fn set_execution_fee(&mut self, lamports: u64) -> &mut Self {
446 self.execution_fee = lamports;
447 self
448 }
449}