gmsol/exchange/
shift.rs

1use std::ops::Deref;
2
3use anchor_client::{
4    anchor_lang::system_program,
5    solana_sdk::{pubkey::Pubkey, signer::Signer},
6};
7use anchor_spl::associated_token::get_associated_token_address;
8use gmsol_solana_utils::{
9    bundle_builder::{BundleBuilder, BundleOptions},
10    transaction_builder::TransactionBuilder,
11};
12use gmsol_store::{
13    accounts, instruction,
14    ops::shift::CreateShiftParams,
15    states::{
16        common::{action::Action, TokensWithFeed},
17        HasMarketMeta, NonceBytes, PriceProviderKind, Shift, Store, TokenMapAccess,
18    },
19};
20use gmsol_utils::market::ordered_tokens;
21
22use crate::{
23    exchange::generate_nonce,
24    store::{token::TokenAccountOps, utils::FeedsParser},
25    utils::{
26        builder::{
27            FeedAddressMap, FeedIds, MakeBundleBuilder, PullOraclePriceConsumer, SetExecutionFee,
28        },
29        fix_optional_account_metas, ZeroCopy,
30    },
31};
32
33use super::ExchangeOps;
34
35/// Create Shift Builder.
36pub struct CreateShiftBuilder<'a, C> {
37    client: &'a crate::Client<C>,
38    store: Pubkey,
39    from_market_token: Pubkey,
40    to_market_token: Pubkey,
41    execution_fee: u64,
42    amount: u64,
43    min_to_market_token_amount: u64,
44    nonce: Option<NonceBytes>,
45    hint: CreateShiftHint,
46    receiver: Pubkey,
47}
48
49/// Hint for creating shift.
50#[derive(Default)]
51pub struct CreateShiftHint {
52    from_market_token_source: Option<Pubkey>,
53}
54
55impl<'a, C: Deref<Target = impl Signer> + Clone> CreateShiftBuilder<'a, C> {
56    pub(super) fn new(
57        client: &'a crate::Client<C>,
58        store: &Pubkey,
59        from_market_token: &Pubkey,
60        to_market_token: &Pubkey,
61        amount: u64,
62    ) -> Self {
63        Self {
64            client,
65            store: *store,
66            from_market_token: *from_market_token,
67            to_market_token: *to_market_token,
68            execution_fee: Shift::MIN_EXECUTION_LAMPORTS,
69            amount,
70            min_to_market_token_amount: 0,
71            nonce: None,
72            hint: Default::default(),
73            receiver: client.payer(),
74        }
75    }
76
77    /// Set the nonce.
78    pub fn nonce(&mut self, nonce: NonceBytes) -> &mut Self {
79        self.nonce = Some(nonce);
80        self
81    }
82
83    /// Set exectuion fee allowed to use.
84    pub fn execution_fee(&mut self, amount: u64) -> &mut Self {
85        self.execution_fee = amount;
86        self
87    }
88
89    /// Set min to market token amount.
90    pub fn min_to_market_token_amount(&mut self, amount: u64) -> &mut Self {
91        self.min_to_market_token_amount = amount;
92        self
93    }
94
95    /// Set hint.
96    pub fn hint(&mut self, hint: CreateShiftHint) -> &mut Self {
97        self.hint = hint;
98        self
99    }
100
101    /// Set receiver.
102    /// Defaults to the payer.
103    pub fn receiver(&mut self, receiver: Pubkey) -> &mut Self {
104        self.receiver = receiver;
105        self
106    }
107
108    fn get_from_market_token_source(&self) -> Pubkey {
109        match self.hint.from_market_token_source {
110            Some(address) => address,
111            None => get_associated_token_address(&self.client.payer(), &self.from_market_token),
112        }
113    }
114
115    fn get_create_shift_params(&self) -> CreateShiftParams {
116        CreateShiftParams {
117            execution_lamports: self.execution_fee,
118            from_market_token_amount: self.amount,
119            min_to_market_token_amount: self.min_to_market_token_amount,
120        }
121    }
122
123    /// Build a [`TransactionBuilder`] to create shift account and return the address of the shift account to create.
124    pub fn build_with_address(&self) -> crate::Result<(TransactionBuilder<'a, C>, Pubkey)> {
125        let token_program_id = anchor_spl::token::ID;
126
127        let owner = self.client.payer();
128        let receiver = self.receiver;
129        let nonce = self.nonce.unwrap_or_else(generate_nonce);
130        let shift = self.client.find_shift_address(&self.store, &owner, &nonce);
131
132        let from_market = self
133            .client
134            .find_market_address(&self.store, &self.from_market_token);
135        let to_market = self
136            .client
137            .find_market_address(&self.store, &self.to_market_token);
138
139        let from_market_token_escrow =
140            get_associated_token_address(&shift, &self.from_market_token);
141        let to_market_token_escrow = get_associated_token_address(&shift, &self.to_market_token);
142        let to_market_token_ata = get_associated_token_address(&receiver, &self.to_market_token);
143
144        let prepare_escrow = self
145            .client
146            .prepare_associated_token_account(
147                &self.from_market_token,
148                &token_program_id,
149                Some(&shift),
150            )
151            .merge(self.client.prepare_associated_token_account(
152                &self.to_market_token,
153                &token_program_id,
154                Some(&shift),
155            ));
156
157        let prepare_ata = self.client.prepare_associated_token_account(
158            &self.to_market_token,
159            &token_program_id,
160            Some(&receiver),
161        );
162
163        let rpc = self
164            .client
165            .store_transaction()
166            .anchor_accounts(accounts::CreateShift {
167                owner,
168                receiver,
169                store: self.store,
170                from_market,
171                to_market,
172                shift,
173                from_market_token: self.from_market_token,
174                to_market_token: self.to_market_token,
175                from_market_token_escrow,
176                to_market_token_escrow,
177                from_market_token_source: self.get_from_market_token_source(),
178                to_market_token_ata,
179                system_program: system_program::ID,
180                token_program: token_program_id,
181                associated_token_program: anchor_spl::associated_token::ID,
182            })
183            .anchor_args(instruction::CreateShift {
184                nonce,
185                params: self.get_create_shift_params(),
186            });
187
188        Ok((prepare_escrow.merge(prepare_ata).merge(rpc), shift))
189    }
190}
191
192/// Close Shift Builder.
193pub struct CloseShiftBuilder<'a, C> {
194    client: &'a crate::Client<C>,
195    shift: Pubkey,
196    reason: String,
197    hint: Option<CloseShiftHint>,
198}
199
200/// Hint for `close_shift` instruction.
201#[derive(Clone)]
202pub struct CloseShiftHint {
203    store: Pubkey,
204    owner: Pubkey,
205    receiver: Pubkey,
206    from_market_token: Pubkey,
207    to_market_token: Pubkey,
208    from_market_token_escrow: Pubkey,
209    to_market_token_escrow: Pubkey,
210}
211
212impl CloseShiftHint {
213    /// Create hint for `close_shift` instruction.
214    pub fn new(shift: &Shift) -> crate::Result<Self> {
215        let tokens = shift.tokens();
216        Ok(Self {
217            store: *shift.header().store(),
218            owner: *shift.header().owner(),
219            receiver: shift.header().receiver(),
220            from_market_token: tokens.from_market_token(),
221            from_market_token_escrow: tokens.from_market_token_account(),
222            to_market_token: tokens.to_market_token(),
223            to_market_token_escrow: tokens.to_market_token_account(),
224        })
225    }
226
227    #[allow(clippy::wrong_self_convention)]
228    fn from_market_token_ata(&self) -> Pubkey {
229        get_associated_token_address(&self.owner, &self.from_market_token)
230    }
231
232    fn to_market_token_ata(&self) -> Pubkey {
233        get_associated_token_address(&self.receiver, &self.to_market_token)
234    }
235}
236
237impl<'a, C: Deref<Target = impl Signer> + Clone> CloseShiftBuilder<'a, C> {
238    pub(super) fn new(client: &'a crate::Client<C>, shift: &Pubkey) -> Self {
239        Self {
240            client,
241            shift: *shift,
242            hint: None,
243            reason: String::from("cancelled"),
244        }
245    }
246
247    /// Set hint.
248    pub fn hint(&mut self, hint: CloseShiftHint) -> &mut Self {
249        self.hint = Some(hint);
250        self
251    }
252
253    /// Set reason.
254    pub fn reason(&mut self, reason: impl ToString) -> &mut Self {
255        self.reason = reason.to_string();
256        self
257    }
258
259    /// Prepare hint if needed
260    pub async fn prepare_hint(&mut self) -> crate::Result<CloseShiftHint> {
261        let hint = match &self.hint {
262            Some(hint) => hint.clone(),
263            None => {
264                let shift = self
265                    .client
266                    .account::<ZeroCopy<_>>(&self.shift)
267                    .await?
268                    .ok_or(crate::Error::NotFound)?;
269                let hint = CloseShiftHint::new(&shift.0)?;
270                self.hint = Some(hint.clone());
271                hint
272            }
273        };
274
275        Ok(hint)
276    }
277
278    /// Build a [`TransactionBuilder`] to close shift account.
279    pub async fn build(&mut self) -> crate::Result<TransactionBuilder<'a, C>> {
280        let hint = self.prepare_hint().await?;
281        let executor = self.client.payer();
282        let rpc = self
283            .client
284            .store_transaction()
285            .anchor_accounts(accounts::CloseShift {
286                executor,
287                store: hint.store,
288                store_wallet: self.client.find_store_wallet_address(&hint.store),
289                owner: hint.owner,
290                receiver: hint.receiver,
291                shift: self.shift,
292                from_market_token: hint.from_market_token,
293                to_market_token: hint.to_market_token,
294                from_market_token_escrow: hint.from_market_token_escrow,
295                to_market_token_escrow: hint.to_market_token_escrow,
296                from_market_token_ata: hint.from_market_token_ata(),
297                to_market_token_ata: hint.to_market_token_ata(),
298                system_program: system_program::ID,
299                token_program: anchor_spl::token::ID,
300                associated_token_program: anchor_spl::associated_token::ID,
301                event_authority: self.client.store_event_authority(),
302                program: *self.client.store_program_id(),
303            })
304            .anchor_args(instruction::CloseShift {
305                reason: self.reason.clone(),
306            });
307
308        Ok(rpc)
309    }
310}
311
312/// Execute Shift Instruction Builder.
313pub struct ExecuteShiftBuilder<'a, C> {
314    client: &'a crate::Client<C>,
315    shift: Pubkey,
316    execution_fee: u64,
317    cancel_on_execution_error: bool,
318    oracle: Pubkey,
319    hint: Option<ExecuteShiftHint>,
320    close: bool,
321    feeds_parser: FeedsParser,
322}
323
324/// Hint for `execute_shift` instruction.
325#[derive(Clone)]
326pub struct ExecuteShiftHint {
327    store: Pubkey,
328    token_map: Pubkey,
329    owner: Pubkey,
330    receiver: Pubkey,
331    from_market_token: Pubkey,
332    to_market_token: Pubkey,
333    from_market_token_escrow: Pubkey,
334    to_market_token_escrow: Pubkey,
335    /// Feeds.
336    pub feeds: TokensWithFeed,
337}
338
339impl ExecuteShiftHint {
340    /// Create hint for `execute_shift` instruction.
341    pub fn new(
342        shift: &Shift,
343        store: &Store,
344        map: &impl TokenMapAccess,
345        from_market: &impl HasMarketMeta,
346        to_market: &impl HasMarketMeta,
347    ) -> crate::Result<Self> {
348        use gmsol_store::states::common::token_with_feeds::token_records;
349
350        let token_infos = shift.tokens();
351
352        let ordered_tokens = ordered_tokens(from_market, to_market);
353        let token_records = token_records(map, &ordered_tokens)?;
354        let feeds = TokensWithFeed::try_from_records(token_records)?;
355
356        Ok(Self {
357            store: *shift.header().store(),
358            owner: *shift.header().owner(),
359            receiver: shift.header().receiver(),
360            from_market_token: token_infos.from_market_token(),
361            from_market_token_escrow: token_infos.from_market_token_account(),
362            to_market_token: token_infos.to_market_token(),
363            to_market_token_escrow: token_infos.to_market_token_account(),
364            token_map: *store.token_map().ok_or(crate::Error::NotFound)?,
365            feeds,
366        })
367    }
368}
369
370impl<'a, C: Deref<Target = impl Signer> + Clone> ExecuteShiftBuilder<'a, C> {
371    pub(super) fn new(
372        client: &'a crate::Client<C>,
373        oracle: &Pubkey,
374        shift: &Pubkey,
375        cancel_on_execution_error: bool,
376    ) -> Self {
377        Self {
378            client,
379            shift: *shift,
380            hint: None,
381            execution_fee: 0,
382            cancel_on_execution_error,
383            oracle: *oracle,
384            close: true,
385            feeds_parser: Default::default(),
386        }
387    }
388
389    /// Set hint.
390    pub fn hint(&mut self, hint: ExecuteShiftHint) -> &mut Self {
391        self.hint = Some(hint);
392        self
393    }
394
395    /// Set whether to cancel the shift account on execution error.
396    pub fn cancel_on_execution_error(&mut self, cancel: bool) -> &mut Self {
397        self.cancel_on_execution_error = cancel;
398        self
399    }
400
401    /// Set whether to close shift account after execution.
402    pub fn close(&mut self, close: bool) -> &mut Self {
403        self.close = close;
404        self
405    }
406
407    async fn prepare_hint(&mut self) -> crate::Result<ExecuteShiftHint> {
408        let hint = match &self.hint {
409            Some(hint) => hint.clone(),
410            None => {
411                let shift = self
412                    .client
413                    .account::<ZeroCopy<Shift>>(&self.shift)
414                    .await?
415                    .ok_or(crate::Error::NotFound)?;
416                let store = self.client.store(shift.0.header().store()).await?;
417                let token_map_address = store
418                    .token_map()
419                    .ok_or(crate::Error::invalid_argument("token map is not set"))?;
420                let token_map = self.client.token_map(token_map_address).await?;
421                let from_market_token = shift.0.tokens().from_market_token();
422                let to_market_token = shift.0.tokens().to_market_token();
423                let from_market = self
424                    .client
425                    .find_market_address(shift.0.header().store(), &from_market_token);
426                let from_market = self.client.market(&from_market).await?;
427                let to_market = self
428                    .client
429                    .find_market_address(shift.0.header().store(), &to_market_token);
430                let to_market = self.client.market(&to_market).await?;
431                let hint = ExecuteShiftHint::new(
432                    &shift.0,
433                    &store,
434                    &token_map,
435                    &*from_market,
436                    &*to_market,
437                )?;
438                self.hint = Some(hint.clone());
439                hint
440            }
441        };
442
443        Ok(hint)
444    }
445
446    /// Build a [`TransactionBuilder`] for `execute_shift` instruction.
447    async fn build_rpc(&mut self) -> crate::Result<TransactionBuilder<'a, C>> {
448        let hint = self.prepare_hint().await?;
449        let authority = self.client.payer();
450
451        let from_market = self
452            .client
453            .find_market_address(&hint.store, &hint.from_market_token);
454        let to_market = self
455            .client
456            .find_market_address(&hint.store, &hint.to_market_token);
457        let from_market_token_vault = self
458            .client
459            .find_market_vault_address(&hint.store, &hint.from_market_token);
460
461        let feeds = self.feeds_parser.parse_and_sort_by_tokens(&hint.feeds)?;
462
463        let mut rpc = self
464            .client
465            .store_transaction()
466            .accounts(fix_optional_account_metas(
467                accounts::ExecuteShift {
468                    authority,
469                    store: hint.store,
470                    token_map: hint.token_map,
471                    oracle: self.oracle,
472                    from_market,
473                    to_market,
474                    shift: self.shift,
475                    from_market_token: hint.from_market_token,
476                    to_market_token: hint.to_market_token,
477                    from_market_token_escrow: hint.from_market_token_escrow,
478                    to_market_token_escrow: hint.to_market_token_escrow,
479                    from_market_token_vault,
480                    token_program: anchor_spl::token::ID,
481                    chainlink_program: None,
482                    event_authority: self.client.store_event_authority(),
483                    program: *self.client.store_program_id(),
484                },
485                &crate::program_ids::DEFAULT_GMSOL_STORE_ID,
486                self.client.store_program_id(),
487            ))
488            .anchor_args(instruction::ExecuteShift {
489                execution_lamports: self.execution_fee,
490                throw_on_execution_error: !self.cancel_on_execution_error,
491            })
492            .accounts(feeds);
493
494        if self.close {
495            let close = self
496                .client
497                .close_shift(&self.shift)
498                .hint(CloseShiftHint {
499                    store: hint.store,
500                    owner: hint.owner,
501                    receiver: hint.receiver,
502                    from_market_token: hint.from_market_token,
503                    to_market_token: hint.to_market_token,
504                    from_market_token_escrow: hint.from_market_token_escrow,
505                    to_market_token_escrow: hint.to_market_token_escrow,
506                })
507                .reason("executed")
508                .build()
509                .await?;
510            rpc = rpc.merge(close);
511        }
512
513        Ok(rpc)
514    }
515}
516
517impl<'a, C: Deref<Target = impl Signer> + Clone> MakeBundleBuilder<'a, C>
518    for ExecuteShiftBuilder<'a, C>
519{
520    async fn build_with_options(
521        &mut self,
522        options: BundleOptions,
523    ) -> gmsol_solana_utils::Result<BundleBuilder<'a, C>> {
524        let mut tx = self.client.bundle_with_options(options);
525        tx.try_push(
526            self.build_rpc()
527                .await
528                .map_err(gmsol_solana_utils::Error::custom)?,
529        )?;
530        Ok(tx)
531    }
532}
533
534impl<C: Deref<Target = impl Signer> + Clone> PullOraclePriceConsumer
535    for ExecuteShiftBuilder<'_, C>
536{
537    async fn feed_ids(&mut self) -> crate::Result<FeedIds> {
538        let hint = self.prepare_hint().await?;
539        Ok(FeedIds::new(hint.store, hint.feeds))
540    }
541
542    fn process_feeds(
543        &mut self,
544        provider: PriceProviderKind,
545        map: FeedAddressMap,
546    ) -> crate::Result<()> {
547        self.feeds_parser
548            .insert_pull_oracle_feed_parser(provider, map);
549        Ok(())
550    }
551}
552
553impl<C> SetExecutionFee for ExecuteShiftBuilder<'_, C> {
554    fn set_execution_fee(&mut self, lamports: u64) -> &mut Self {
555        self.execution_fee = lamports;
556        self
557    }
558}