gmsol/exchange/
position_cut.rs

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
37/// The compute budget for `position_cut` instruction.
38pub const POSITION_CUT_COMPUTE_BUDGET: u32 = 400_000;
39
40/// `PositionCut` instruction builder.
41pub 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/// Hint for `PositionCut`.
58#[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    /// Create from position.
76    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    /// Create a new hint.
109    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    /// Get feeds.
153    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    /// Prepare hint for position cut.
183    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    /// Set whether to close the order after the execution.
196    pub fn close(&mut self, close: bool) -> &mut Self {
197        self.close = close;
198        self
199    }
200
201    /// Set event buffer index.
202    pub fn event_buffer_index(&mut self, index: u16) -> &mut Self {
203        self.event_buffer_index = index;
204        self
205    }
206
207    /// Set hint with the given position for position cut.
208    pub fn hint(&mut self, hint: PositionCutHint) -> &mut Self {
209        self.hint = Some(hint);
210        self
211    }
212
213    /// Set price provider to the given.
214    pub fn price_provider(&mut self, program: &Pubkey) -> &mut Self {
215        self.price_provider = *program;
216        self
217    }
218
219    /// Insert an Address Lookup Table.
220    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}