drift_rs/
jit_client.rs

1//! JIT proxy client
2//!
3//! Routes JIT maker orders via onchain jit-proxy program
4use std::borrow::Cow;
5
6use anchor_lang::{
7    prelude::borsh::{self, BorshDeserialize, BorshSerialize},
8    AnchorDeserialize, AnchorSerialize, InstructionData,
9};
10use solana_rpc_client_api::config::RpcSendTransactionConfig;
11use solana_sdk::{
12    compute_budget::ComputeBudgetInstruction,
13    instruction::{AccountMeta, Instruction},
14    message::{v0, VersionedMessage},
15    pubkey::Pubkey,
16    signature::Signature,
17};
18
19use crate::{
20    accounts::User,
21    build_accounts,
22    constants::{self, state_account, JIT_PROXY_ID},
23    drift_idl,
24    swift_order_subscriber::SignedOrderInfo,
25    types::PositionDirection,
26    DriftClient, MarketId, MarketType, PostOnlyParam, ReferrerInfo, SdkError, SdkResult,
27    TransactionBuilder, Wallet,
28};
29
30#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)]
31pub enum PriceType {
32    Limit,
33    Oracle,
34}
35
36/// Taker account parameters for JIT tx building
37pub struct JitTakerParams {
38    taker: User,
39    taker_key: Pubkey,
40    taker_stats_key: Pubkey,
41    taker_referrer_info: Option<ReferrerInfo>,
42}
43
44impl JitTakerParams {
45    pub fn new(
46        taker_key: Pubkey,
47        taker_stats_key: Pubkey,
48        taker: User,
49        taker_referrer_info: Option<ReferrerInfo>,
50    ) -> Self {
51        Self {
52            taker_key,
53            taker_stats_key,
54            taker,
55            taker_referrer_info,
56        }
57    }
58}
59
60#[derive(Copy, Clone, Debug)]
61/// Parameters for building a jit maker order
62pub struct JitIxParams {
63    pub max_position: i64,
64    pub min_position: i64,
65    pub bid: i64,
66    pub ask: i64,
67    pub price_type: PriceType,
68    pub post_only: Option<PostOnlyParam>,
69}
70
71impl JitIxParams {
72    pub fn new(
73        max_position: i64,
74        min_position: i64,
75        bid: i64,
76        ask: i64,
77        price_type: PriceType,
78        post_only: Option<PostOnlyParam>,
79    ) -> Self {
80        Self {
81            max_position,
82            min_position,
83            bid,
84            ask,
85            price_type,
86            post_only,
87        }
88    }
89}
90
91#[derive(Clone)]
92pub struct JitProxyClient {
93    drift_client: DriftClient,
94    config: RpcSendTransactionConfig,
95    cu_params: Option<ComputeBudgetParams>,
96}
97
98impl JitProxyClient {
99    pub fn new(
100        drift_client: DriftClient,
101        config: Option<RpcSendTransactionConfig>,
102        cu_params: Option<ComputeBudgetParams>,
103    ) -> Self {
104        Self {
105            drift_client,
106            config: config.unwrap_or_default(),
107            cu_params,
108        }
109    }
110
111    pub fn update_config(&mut self, config: RpcSendTransactionConfig) {
112        self.config = config;
113    }
114
115    pub fn update_cu_params(&mut self, cu_params: ComputeBudgetParams) {
116        self.cu_params = Some(cu_params);
117    }
118
119    /// Build a jit tx
120    ///
121    /// `taker_params` JIT taker account params
122    /// `taker_order_id` order Id of the JIT order
123    /// `jit_ix_params` bounds for the JIT fill
124    /// `maker_params` tuple (pubkey, data) of maker's sub-account
125    pub async fn build_jit_tx(
126        &self,
127        taker_order_id: u32,
128        taker_params: &JitTakerParams,
129        jit_ix_params: JitIxParams,
130        maker_params: (&Pubkey, &User),
131    ) -> SdkResult<VersionedMessage> {
132        let order = taker_params
133            .taker
134            .orders
135            .iter()
136            .find(|order| order.order_id == taker_order_id)
137            .ok_or(SdkError::JitOrderNotFound)?;
138
139        let tx_builder = TransactionBuilder::new(
140            self.drift_client.program_data(),
141            *maker_params.0,
142            Cow::Borrowed(maker_params.1),
143            false,
144        );
145
146        let program_data = tx_builder.program_data();
147        let account_data = tx_builder.account_data();
148
149        let writable_markets = match order.market_type {
150            MarketType::Perp => {
151                vec![MarketId::perp(order.market_index)]
152            }
153            MarketType::Spot => {
154                vec![MarketId::spot(order.market_index), MarketId::QUOTE_SPOT]
155            }
156        };
157
158        let maker_authority = maker_params.1.authority;
159        let mut accounts = build_accounts(
160            program_data,
161            self::accounts::Jit {
162                state: *state_account(),
163                user: *maker_params.0,
164                user_stats: Wallet::derive_stats_account(&maker_authority),
165                taker: taker_params.taker_key,
166                taker_stats: taker_params.taker_stats_key,
167                authority: maker_authority,
168                drift_program: constants::PROGRAM_ID,
169            },
170            &[&taker_params.taker, account_data],
171            std::iter::empty(),
172            writable_markets.iter(),
173        );
174
175        if let Some(referrer_info) = taker_params.taker_referrer_info {
176            accounts.push(AccountMeta::new(referrer_info.referrer(), false));
177            accounts.push(AccountMeta::new(referrer_info.referrer_stats(), false));
178        }
179
180        if order.market_type == drift_idl::types::MarketType::Spot {
181            let spot_market_vault = self
182                .drift_client
183                .try_get_spot_market_account(order.market_index)?
184                .vault;
185            let quote_spot_market_vault = self
186                .drift_client
187                .try_get_spot_market_account(MarketId::QUOTE_SPOT.index())?
188                .vault;
189            accounts.push(AccountMeta::new_readonly(spot_market_vault, false));
190            accounts.push(AccountMeta::new_readonly(quote_spot_market_vault, false));
191        }
192
193        let jit_params = self::instruction::JitParams {
194            taker_order_id,
195            max_position: jit_ix_params.max_position,
196            min_position: jit_ix_params.min_position,
197            bid: jit_ix_params.bid,
198            ask: jit_ix_params.ask,
199            price_type: jit_ix_params.price_type,
200            post_only: jit_ix_params.post_only,
201        };
202
203        let ix = Instruction {
204            program_id: JIT_PROXY_ID,
205            accounts,
206            data: instruction::Jit { params: jit_params }.data(),
207        };
208
209        let mut ixs = Vec::with_capacity(3);
210        if let Some(cu_params) = self.cu_params {
211            let cu_limit_ix =
212                ComputeBudgetInstruction::set_compute_unit_price(cu_params.microlamports_per_cu());
213            let cu_price_ix =
214                ComputeBudgetInstruction::set_compute_unit_limit(cu_params.cu_limit());
215
216            ixs.push(cu_limit_ix);
217            ixs.push(cu_price_ix);
218        }
219        ixs.push(ix);
220
221        let luts = program_data.lookup_tables;
222
223        let message =
224            v0::Message::try_compile(&maker_authority, ixs.as_slice(), luts, Default::default())
225                .expect("failed to compile message");
226
227        Ok(VersionedMessage::V0(message))
228    }
229
230    /// Build a swift fill tx against a taker order given by `taker_params`
231    ///
232    /// `signed_order_info` Fastlane (order) message to place-and-make against
233    /// `taker_params` taker account params
234    /// `jit_params` config for the JIT proxy
235    /// `maker_pubkey` address of the maker's subaccount
236    /// `maker_account_data` Maker's (User) account data corresponding with the `maker_pubkey`
237    ///
238    /// Returns a Solana `VersionedMessage` ready for signing
239    pub async fn build_swift_ix(
240        &self,
241        signed_order_info: &SignedOrderInfo,
242        taker_params: &JitTakerParams,
243        jit_ix_params: &JitIxParams,
244        maker_pubkey: &Pubkey,
245        maker_account_data: &User,
246    ) -> SdkResult<VersionedMessage> {
247        let maker_authority = maker_account_data.authority;
248        let program_data = self.drift_client.program_data();
249        let signed_order_info_params = signed_order_info.order_params();
250        let account_data = maker_account_data;
251        let market_index = signed_order_info_params.market_index;
252        let market_type = signed_order_info_params.market_type;
253
254        let writable_markets = match market_type {
255            MarketType::Perp => {
256                vec![MarketId::perp(market_index)]
257            }
258            MarketType::Spot => {
259                vec![MarketId::spot(market_index), MarketId::QUOTE_SPOT]
260            }
261        };
262
263        let mut accounts = build_accounts(
264            program_data,
265            self::accounts::JitSignedMsg {
266                state: *state_account(),
267                authority: maker_authority,
268                user: *maker_pubkey,
269                user_stats: Wallet::derive_stats_account(&maker_authority),
270                taker: taker_params.taker_key,
271                taker_stats: taker_params.taker_stats_key,
272                taker_signed_msg_user_orders: Wallet::derive_swift_order_account(
273                    &taker_params.taker.authority,
274                ),
275                drift_program: constants::PROGRAM_ID,
276            },
277            &[&taker_params.taker, account_data],
278            std::iter::empty(),
279            writable_markets.iter(),
280        );
281
282        if let Some(referrer_info) = taker_params.taker_referrer_info {
283            accounts.push(AccountMeta::new(referrer_info.referrer(), false));
284            accounts.push(AccountMeta::new(referrer_info.referrer_stats(), false));
285        }
286
287        if market_type == drift_idl::types::MarketType::Spot {
288            let spot_market_vault = self
289                .drift_client
290                .try_get_spot_market_account(market_index)?
291                .vault;
292            let quote_spot_market_vault = self
293                .drift_client
294                .try_get_spot_market_account(MarketId::QUOTE_SPOT.index())?
295                .vault;
296            accounts.push(AccountMeta::new_readonly(spot_market_vault, false));
297            accounts.push(AccountMeta::new_readonly(quote_spot_market_vault, false));
298        }
299
300        let jit_params = self::instruction::JitSignedMsgParams {
301            signed_order_info_uuid: signed_order_info.order_uuid(),
302            max_position: jit_ix_params.max_position,
303            min_position: jit_ix_params.min_position,
304            bid: jit_ix_params.bid,
305            ask: jit_ix_params.ask,
306            price_type: jit_ix_params.price_type,
307            post_only: jit_ix_params.post_only,
308        };
309
310        let fill_ix = Instruction {
311            program_id: JIT_PROXY_ID,
312            accounts,
313            data: instruction::JitSignedMsg { params: jit_params }.data(),
314        };
315
316        let message = TransactionBuilder::new(
317            self.drift_client.program_data(),
318            *maker_pubkey,
319            Cow::Borrowed(maker_account_data),
320            false,
321        )
322        .place_swift_order(signed_order_info, &taker_params.taker)
323        .add_ix(fill_ix)
324        .build();
325
326        Ok(message)
327    }
328
329    /// Send a jit tx with given params
330    ///
331    /// `taker_order_id` Id of the order to take against
332    /// `taker_params` taker account data for the tx
333    /// `jit_params` bounds for the JIT fill
334    /// `maker_authority` the maker's authority key
335    /// `sub_account_id` the maker's sub-account for the fill
336    pub async fn jit(
337        &self,
338        taker_order_id: u32,
339        taker_params: &JitTakerParams,
340        jit_params: JitIxParams,
341        maker_authority: &Pubkey,
342        sub_account_id: Option<u16>,
343    ) -> SdkResult<Signature> {
344        let sub_account =
345            Wallet::derive_user_account(maker_authority, sub_account_id.unwrap_or_default());
346        let sub_account_data = self.drift_client.get_user_account(&sub_account).await?;
347        let tx = self
348            .build_jit_tx(
349                taker_order_id,
350                taker_params,
351                jit_params,
352                (&sub_account, &sub_account_data),
353            )
354            .await?;
355        self.drift_client
356            .sign_and_send_with_config(tx, None, self.config)
357            .await
358    }
359
360    /// Try fill against a swift order with JIT-proxy protection
361    ///
362    /// `signed_order_info` the swift order info
363    /// `taker_params` taker account data for the tx
364    /// `jit_params` bounds for the JIT fill
365    /// `maker_authority` the maker's authority key
366    /// `sub_account_id` the maker's sub-account for the fill
367    pub async fn try_swift_fill(
368        &self,
369        signed_order_info: &SignedOrderInfo,
370        taker_params: &JitTakerParams,
371        jit_params: &JitIxParams,
372        maker_authority: &Pubkey,
373        sub_account_id: Option<u16>,
374    ) -> SdkResult<Signature> {
375        let sub_account =
376            Wallet::derive_user_account(maker_authority, sub_account_id.unwrap_or_default());
377        let sub_account_data = self.drift_client.get_user_account(&sub_account).await?;
378        let tx = self
379            .build_swift_ix(
380                signed_order_info,
381                taker_params,
382                jit_params,
383                &sub_account,
384                &sub_account_data,
385            )
386            .await?;
387        self.drift_client
388            .sign_and_send_with_config(tx, None, self.config)
389            .await
390    }
391}
392
393#[derive(Clone, Copy)]
394pub struct ComputeBudgetParams {
395    microlamports_per_cu: u64,
396    cu_limit: u32,
397}
398
399impl ComputeBudgetParams {
400    pub fn new(microlamports_per_cu: u64, cu_limit: u32) -> Self {
401        Self {
402            microlamports_per_cu,
403            cu_limit,
404        }
405    }
406
407    pub fn microlamports_per_cu(&self) -> u64 {
408        self.microlamports_per_cu
409    }
410
411    pub fn cu_limit(&self) -> u32 {
412        self.cu_limit
413    }
414}
415
416#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq)]
417pub struct JitSwiftParams {
418    pub signed_order_info_uuid: [u8; 8],
419    pub max_position: i64,
420    pub min_position: i64,
421    pub bid: i64,
422    pub ask: i64,
423    pub price_type: PriceType,
424    pub post_only: Option<PostOnlyParam>,
425}
426
427impl Default for JitSwiftParams {
428    fn default() -> Self {
429        Self {
430            signed_order_info_uuid: [0; 8],
431            max_position: 0,
432            min_position: 0,
433            bid: 0,
434            ask: 0,
435            price_type: PriceType::Limit,
436            post_only: None,
437        }
438    }
439}
440
441impl JitSwiftParams {
442    pub fn get_worst_price(
443        self,
444        oracle_price: i64,
445        taker_direction: PositionDirection,
446    ) -> SdkResult<u64> {
447        match (taker_direction, self.price_type) {
448            (PositionDirection::Long, PriceType::Limit) => Ok(self.ask.unsigned_abs()),
449            (PositionDirection::Short, PriceType::Limit) => Ok(self.bid.unsigned_abs()),
450            (PositionDirection::Long, PriceType::Oracle) => {
451                Ok(oracle_price.saturating_add(self.ask).unsigned_abs())
452            }
453            (PositionDirection::Short, PriceType::Oracle) => {
454                Ok(oracle_price.saturating_add(self.bid).unsigned_abs())
455            }
456        }
457    }
458}
459
460pub mod instruction {
461    //! copied from jit-proxy program
462    //! simplifies dependency graph, unlikely to change frequently
463    use super::*;
464    use crate::PostOnlyParam;
465    #[derive(BorshDeserialize, BorshSerialize)]
466    pub struct Jit {
467        pub params: JitParams,
468    }
469    impl anchor_lang::Discriminator for Jit {
470        const DISCRIMINATOR: &[u8] = &[99, 42, 97, 140, 152, 62, 167, 234];
471    }
472    impl anchor_lang::InstructionData for Jit {}
473
474    #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)]
475    pub struct JitParams {
476        pub taker_order_id: u32,
477        pub max_position: i64,
478        pub min_position: i64,
479        pub bid: i64,
480        pub ask: i64,
481        pub price_type: PriceType,
482        pub post_only: Option<PostOnlyParam>,
483    }
484
485    #[derive(BorshDeserialize, BorshSerialize)]
486    pub struct JitSignedMsg {
487        pub params: JitSignedMsgParams,
488    }
489    impl anchor_lang::Discriminator for JitSignedMsg {
490        const DISCRIMINATOR: &[u8] = &[134, 130, 156, 72, 37, 120, 153, 21];
491    }
492    impl anchor_lang::InstructionData for JitSignedMsg {}
493
494    #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)]
495    pub struct JitSignedMsgParams {
496        pub signed_order_info_uuid: [u8; 8],
497        pub max_position: i64,
498        pub min_position: i64,
499        pub bid: i64,
500        pub ask: i64,
501        pub price_type: PriceType,
502        pub post_only: Option<PostOnlyParam>,
503    }
504}
505
506pub mod accounts {
507    //! copied from jit-proxy program
508    //! simplifies dependency graph, unlikely to change frequently
509    use solana_sdk::instruction::AccountMeta;
510
511    use super::*;
512    use crate::drift_idl::traits::ToAccountMetas;
513
514    /// this is generated from `#[derive(Accounts)]` from `__client_accounts_jit`
515    #[derive(anchor_lang::AnchorSerialize)]
516    pub struct Jit {
517        pub state: Pubkey,
518        pub user: Pubkey,
519        pub user_stats: Pubkey,
520        pub taker: Pubkey,
521        pub taker_stats: Pubkey,
522        pub authority: Pubkey,
523        pub drift_program: Pubkey,
524    }
525    #[automatically_derived]
526    impl ToAccountMetas for Jit {
527        fn to_account_metas(&self) -> Vec<AccountMeta> {
528            vec![
529                AccountMeta::new_readonly(self.state, false),
530                AccountMeta::new(self.user, false),
531                AccountMeta::new(self.user_stats, false),
532                AccountMeta::new(self.taker, false),
533                AccountMeta::new(self.taker_stats, false),
534                AccountMeta::new_readonly(self.authority, true),
535                AccountMeta::new_readonly(self.drift_program, false),
536            ]
537        }
538    }
539
540    pub struct JitSignedMsg {
541        pub state: Pubkey,
542        pub user: Pubkey,
543        pub user_stats: Pubkey,
544        pub taker: Pubkey,
545        pub taker_stats: Pubkey,
546        pub taker_signed_msg_user_orders: Pubkey,
547        pub authority: Pubkey,
548        pub drift_program: Pubkey,
549    }
550    #[automatically_derived]
551    impl ToAccountMetas for JitSignedMsg {
552        fn to_account_metas(&self) -> Vec<AccountMeta> {
553            vec![
554                AccountMeta::new_readonly(self.state, false),
555                AccountMeta::new(self.user, false),
556                AccountMeta::new(self.user_stats, false),
557                AccountMeta::new(self.taker, false),
558                AccountMeta::new(self.taker_stats, false),
559                AccountMeta::new(self.taker_signed_msg_user_orders, false),
560                AccountMeta::new_readonly(self.authority, true),
561                AccountMeta::new_readonly(self.drift_program, false),
562            ]
563        }
564    }
565}