Skip to main content

gmsol_sdk/builders/glv_withdrawal/
create.rs

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