gmsol_sdk/builders/glv_withdrawal/
create.rs1use 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#[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 #[cfg_attr(serde, serde(default))]
31 #[builder(default)]
32 pub program: StoreProgram,
33 #[builder(setter(into))]
35 pub payer: StringPubkey,
36 #[cfg_attr(serde, serde(default))]
38 #[builder(default, setter(strip_option, into))]
39 pub receiver: Option<StringPubkey>,
40 #[cfg_attr(serde, serde(default))]
42 #[builder(default, setter(strip_option, into))]
43 pub nonce: Option<NonceBytes>,
44 #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
46 #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_GLV_WITHDRAWAL)]
47 pub execution_lamports: u64,
48 #[builder(setter(into))]
50 pub glv_token: StringPubkey,
51 #[builder(setter(into))]
53 pub market_token: StringPubkey,
54 #[cfg_attr(serde, serde(default))]
56 #[builder(default, setter(into))]
57 pub glv_token_account: Option<StringPubkey>,
58 #[cfg_attr(serde, serde(default))]
60 #[builder(default, setter(into))]
61 pub long_receive_token: Option<StringPubkey>,
62 #[cfg_attr(serde, serde(default))]
64 #[builder(default, setter(into))]
65 pub long_swap_path: Vec<StringPubkey>,
66 #[cfg_attr(serde, serde(default))]
68 #[builder(default, setter(into))]
69 pub short_receive_token: Option<StringPubkey>,
70 #[cfg_attr(serde, serde(default))]
72 #[builder(default, setter(into))]
73 pub short_swap_path: Vec<StringPubkey>,
74 #[cfg_attr(serde, serde(default))]
76 #[builder(default)]
77 pub glv_token_amount: u64,
78 #[cfg_attr(serde, serde(default))]
80 #[builder(default)]
81 pub min_long_receive_amount: u64,
82 #[cfg_attr(serde, serde(default))]
84 #[builder(default)]
85 pub min_short_receive_amount: u64,
86 #[cfg_attr(serde, serde(default))]
88 #[builder(default)]
89 pub unwrap_native_on_receive: bool,
90 #[cfg_attr(serde, serde(default))]
92 #[builder(default)]
93 pub skip_long_receive_token_ata_creation: bool,
94 #[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#[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 #[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}