Skip to main content

gmsol_sdk/builders/glv_deposit/
create.rs

1use anchor_spl::associated_token::{self, get_associated_token_address_with_program_id};
2use gmsol_model::num_traits::Zero;
3use gmsol_programs::gmsol_store::{
4    client::{accounts, args},
5    types::CreateGlvDepositParams,
6};
7use gmsol_solana_utils::{
8    client_traits::FromRpcClientWith, AtomicGroup, IntoAtomicGroup, ProgramExt,
9};
10use solana_sdk::{instruction::AccountMeta, system_program};
11use typed_builder::TypedBuilder;
12
13use crate::{
14    builders::{
15        glv_deposit::MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT,
16        utils::{generate_nonce, prepare_ata},
17        MarketTokenIxBuilder, NonceBytes, PoolTokenHint, StoreProgram, StoreProgramIxBuilder,
18    },
19    serde::StringPubkey,
20};
21
22/// Builder for the `create_glv_deposit` instruction.
23#[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, TypedBuilder)]
27pub struct CreateGlvDeposit {
28    /// Program.
29    #[cfg_attr(serde, serde(default))]
30    #[builder(default)]
31    pub program: StoreProgram,
32    /// Payer (a.k.a. owner).
33    #[builder(setter(into))]
34    pub payer: StringPubkey,
35    /// Reciever.
36    #[cfg_attr(serde, serde(default))]
37    #[builder(default, setter(strip_option, into))]
38    pub receiver: Option<StringPubkey>,
39    /// Nonce for the deposit.
40    #[cfg_attr(serde, serde(default))]
41    #[builder(default, setter(strip_option, into))]
42    pub nonce: Option<NonceBytes>,
43    /// The GLV token.
44    #[builder(setter(into))]
45    pub glv_token: StringPubkey,
46    /// The market token of the market in which the deposit will be created.
47    #[builder(setter(into))]
48    pub market_token: StringPubkey,
49    /// Execution fee paid to the keeper in lamports.
50    #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
51    #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT)]
52    pub execution_lamports: u64,
53    /// Long pay token.
54    #[cfg_attr(serde, serde(default))]
55    #[builder(default, setter(into))]
56    pub long_pay_token: Option<StringPubkey>,
57    /// Long pay token account.
58    #[cfg_attr(serde, serde(default))]
59    #[builder(default, setter(into))]
60    pub long_pay_token_account: Option<StringPubkey>,
61    /// Swap path for long pay token.
62    #[cfg_attr(serde, serde(default))]
63    #[builder(default, setter(into))]
64    pub long_swap_path: Vec<StringPubkey>,
65    /// Short pay token.
66    #[cfg_attr(serde, serde(default))]
67    #[builder(default, setter(into))]
68    pub short_pay_token: Option<StringPubkey>,
69    /// Short pay token account.
70    #[cfg_attr(serde, serde(default))]
71    #[builder(default, setter(into))]
72    pub short_pay_token_account: Option<StringPubkey>,
73    /// Swap path for short pay token.
74    #[cfg_attr(serde, serde(default))]
75    #[builder(default, setter(into))]
76    pub short_swap_path: Vec<StringPubkey>,
77    /// Market token account.
78    #[cfg_attr(serde, serde(default))]
79    #[builder(default, setter(into))]
80    pub market_token_account: Option<StringPubkey>,
81    /// Long pay token amount.
82    #[cfg_attr(serde, serde(default))]
83    #[builder(default)]
84    pub long_pay_amount: u64,
85    /// Short pay token amount.
86    #[cfg_attr(serde, serde(default))]
87    #[builder(default)]
88    pub short_pay_amount: u64,
89    /// Market token amount to pay.
90    #[cfg_attr(serde, serde(default))]
91    #[builder(default)]
92    pub market_token_amount: u64,
93    /// Minimum amount of output market tokens.
94    #[cfg_attr(serde, serde(default))]
95    #[builder(default)]
96    pub min_market_token_amount: u64,
97    /// Minimum amount of output GLV tokens.
98    #[cfg_attr(serde, serde(default))]
99    #[builder(default)]
100    pub min_receive_amount: u64,
101    /// Whether to unwrap the native token when receiving (e.g., convert WSOL to SOL).
102    #[cfg_attr(serde, serde(default))]
103    #[builder(default)]
104    pub unwrap_native_on_receive: bool,
105    /// Whether to skip the creation of GLV token ATA.
106    #[cfg_attr(serde, serde(default))]
107    #[builder(default)]
108    pub skip_glv_token_ata_creation: bool,
109}
110
111#[cfg(serde)]
112fn default_execution_lamports() -> u64 {
113    MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT
114}
115
116impl StoreProgramIxBuilder for CreateGlvDeposit {
117    fn store_program(&self) -> &StoreProgram {
118        &self.program
119    }
120}
121
122impl MarketTokenIxBuilder for CreateGlvDeposit {
123    fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
124        &self.market_token
125    }
126}
127
128impl IntoAtomicGroup for CreateGlvDeposit {
129    type Hint = CreateGlvDepositHint;
130
131    fn into_atomic_group(self, hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
132        if self.long_pay_amount.is_zero()
133            && self.short_pay_amount.is_zero()
134            && self.market_token_amount.is_zero()
135        {
136            return Err(gmsol_solana_utils::Error::custom(
137                "invalid argument: empty GLV deposit",
138            ));
139        }
140
141        let owner = self.payer.0;
142        let mut insts = AtomicGroup::new(&owner);
143
144        let receiver = self.receiver.as_deref().copied().unwrap_or(owner);
145        let nonce = self.nonce.unwrap_or_else(generate_nonce);
146        let glv_deposit = self.program.find_glv_deposit_address(&owner, &nonce);
147        let token_program_id = anchor_spl::token::ID;
148        let glv_token_program_id = anchor_spl::token_2022::ID;
149        let market_token = self.market_token.0;
150        let glv_token = self.glv_token.0;
151
152        let long_pay_token = (!self.long_pay_amount.is_zero()).then(|| {
153            self.long_pay_token
154                .as_deref()
155                .unwrap_or(&hint.pool_tokens.long_token)
156        });
157        let long_pay_token_account = long_pay_token.as_ref().map(|token| {
158            self.long_pay_token_account
159                .as_deref()
160                .copied()
161                .unwrap_or_else(|| {
162                    get_associated_token_address_with_program_id(&owner, token, &token_program_id)
163                })
164        });
165        let short_pay_token = (!self.short_pay_amount.is_zero()).then(|| {
166            self.short_pay_token
167                .as_deref()
168                .unwrap_or(&hint.pool_tokens.short_token)
169        });
170        let short_pay_token_account = short_pay_token.as_ref().map(|token| {
171            self.short_pay_token_account
172                .as_deref()
173                .copied()
174                .unwrap_or_else(|| {
175                    get_associated_token_address_with_program_id(&owner, token, &token_program_id)
176                })
177        });
178        let market_token_account = (!self.market_token_amount.is_zero()).then(|| {
179            self.market_token_account
180                .as_deref()
181                .copied()
182                .unwrap_or_else(|| {
183                    get_associated_token_address_with_program_id(
184                        &owner,
185                        &market_token,
186                        &token_program_id,
187                    )
188                })
189        });
190
191        let (long_pay_token_escrow, prepare) =
192            prepare_ata(&owner, &glv_deposit, long_pay_token, &token_program_id).unzip();
193        insts.extend(prepare);
194
195        let (short_pay_token_escrow, prepare) =
196            prepare_ata(&owner, &glv_deposit, short_pay_token, &token_program_id).unzip();
197        insts.extend(prepare);
198
199        let (market_token_escrow, prepare) =
200            prepare_ata(&owner, &glv_deposit, Some(&market_token), &token_program_id)
201                .expect("must exist");
202        insts.add(prepare);
203
204        let (glv_token_escrow, prepare) = prepare_ata(
205            &owner,
206            &glv_deposit,
207            Some(&glv_token),
208            &glv_token_program_id,
209        )
210        .expect("must exist");
211        insts.add(prepare);
212
213        let (_glv_token_ata, prepare) =
214            prepare_ata(&owner, &receiver, Some(&glv_token), &glv_token_program_id)
215                .expect("must exist");
216        if !self.skip_glv_token_ata_creation {
217            insts.add(prepare);
218        }
219
220        let params = CreateGlvDepositParams {
221            execution_lamports: self.execution_lamports,
222            long_token_swap_length: self
223                .long_swap_path
224                .len()
225                .try_into()
226                .map_err(gmsol_solana_utils::Error::custom)?,
227            short_token_swap_length: self
228                .short_swap_path
229                .len()
230                .try_into()
231                .map_err(gmsol_solana_utils::Error::custom)?,
232            initial_long_token_amount: self.long_pay_amount,
233            initial_short_token_amount: self.short_pay_amount,
234            market_token_amount: self.market_token_amount,
235            min_market_token_amount: self.min_market_token_amount,
236            min_glv_token_amount: self.min_receive_amount,
237            should_unwrap_native_token: self.unwrap_native_on_receive,
238        };
239
240        let create = self
241            .program
242            .anchor_instruction(args::CreateGlvDeposit {
243                nonce: nonce.to_bytes(),
244                params,
245            })
246            .anchor_accounts(
247                accounts::CreateGlvDeposit {
248                    owner,
249                    receiver,
250                    store: self.program.store.0,
251                    glv: self.program.find_glv_address(&glv_token),
252                    market: self.program.find_market_address(&market_token),
253                    glv_deposit,
254                    glv_token,
255                    market_token,
256                    initial_long_token: long_pay_token.copied(),
257                    initial_short_token: short_pay_token.copied(),
258                    glv_token_escrow,
259                    market_token_escrow,
260                    initial_long_token_escrow: long_pay_token_escrow,
261                    initial_short_token_escrow: short_pay_token_escrow,
262                    initial_long_token_source: long_pay_token_account,
263                    initial_short_token_source: short_pay_token_account,
264                    market_token_source: market_token_account,
265                    system_program: system_program::ID,
266                    token_program: token_program_id,
267                    glv_token_program: glv_token_program_id,
268                    associated_token_program: associated_token::ID,
269                },
270                true,
271            )
272            .accounts(
273                self.long_swap_path
274                    .iter()
275                    .chain(self.short_swap_path.iter())
276                    .map(|token| AccountMeta {
277                        pubkey: self.program.find_market_address(token),
278                        is_signer: false,
279                        is_writable: false,
280                    })
281                    .collect::<Vec<_>>(),
282            )
283            .build();
284        insts.add(create);
285
286        Ok(insts)
287    }
288}
289
290/// Hint for [`CreateGlvDeposit`].
291#[cfg_attr(js, derive(tsify_next::Tsify))]
292#[cfg_attr(js, tsify(from_wasm_abi))]
293#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
294#[derive(Debug, Clone, TypedBuilder)]
295pub struct CreateGlvDepositHint {
296    /// Pool tokens.
297    #[builder(setter(into))]
298    pub pool_tokens: PoolTokenHint,
299}
300
301impl FromRpcClientWith<CreateGlvDeposit> for CreateGlvDepositHint {
302    async fn from_rpc_client_with<'a>(
303        builder: &'a CreateGlvDeposit,
304        client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
305    ) -> gmsol_solana_utils::Result<Self> {
306        let pool_tokens = PoolTokenHint::from_rpc_client_with(builder, client).await?;
307        Ok(Self { pool_tokens })
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    #[cfg(not(target_arch = "wasm32"))]
314    use tokio::test as async_test;
315
316    #[cfg(target_arch = "wasm32")]
317    use wasm_bindgen_test::wasm_bindgen_test as async_test;
318
319    use gmsol_solana_utils::{
320        client_traits::GenericRpcClient, cluster::Cluster, transaction_builder::default_before_sign,
321    };
322    use solana_sdk::pubkey::Pubkey;
323
324    use super::*;
325
326    #[test]
327    fn create_glv_deposit() -> crate::Result<()> {
328        let long_token = Pubkey::new_unique();
329        let short_token = Pubkey::new_unique();
330        CreateGlvDeposit::builder()
331            .payer(Pubkey::new_unique())
332            .long_swap_path([Pubkey::new_unique().into()])
333            .long_pay_amount(1_000_000_000)
334            .long_pay_token(Some(Pubkey::new_unique().into()))
335            .glv_token(Pubkey::new_unique())
336            .market_token(Pubkey::new_unique())
337            .unwrap_native_on_receive(true)
338            .build()
339            .into_atomic_group(
340                &CreateGlvDepositHint::builder()
341                    .pool_tokens(
342                        PoolTokenHint::builder()
343                            .long_token(long_token)
344                            .short_token(short_token)
345                            .build(),
346                    )
347                    .build(),
348            )?
349            .partially_signed_transaction_with_blockhash_and_options(
350                Default::default(),
351                Default::default(),
352                None,
353                default_before_sign,
354            )?;
355        Ok(())
356    }
357
358    #[async_test]
359    async fn create_glv_deposit_with_rpc() -> crate::Result<()> {
360        let market_token: Pubkey = "5sdFW7wrKsxxYHMXoqDmNHkGyCWsbLEFb1x1gzBBm4Hx".parse()?;
361        let wsol: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
362
363        let cluster = Cluster::Devnet;
364        let client = GenericRpcClient::new(cluster.url());
365
366        CreateGlvDeposit::builder()
367            .payer(Pubkey::new_unique())
368            .short_swap_path([Pubkey::new_unique().into()])
369            .short_pay_amount(1_000_000_000)
370            .short_pay_token(Some(wsol.into()))
371            .glv_token(Pubkey::new_unique())
372            .market_token(market_token)
373            .unwrap_native_on_receive(true)
374            .build()
375            .into_atomic_group_with_rpc_client(&client)
376            .await?
377            .partially_signed_transaction_with_blockhash_and_options(
378                Default::default(),
379                Default::default(),
380                None,
381                default_before_sign,
382            )?;
383
384        Ok(())
385    }
386}