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#[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 #[cfg_attr(serde, serde(default))]
30 #[builder(default)]
31 pub program: StoreProgram,
32 #[builder(setter(into))]
34 pub payer: StringPubkey,
35 #[cfg_attr(serde, serde(default))]
37 #[builder(default, setter(strip_option, into))]
38 pub receiver: Option<StringPubkey>,
39 #[cfg_attr(serde, serde(default))]
41 #[builder(default, setter(strip_option, into))]
42 pub nonce: Option<NonceBytes>,
43 #[builder(setter(into))]
45 pub glv_token: StringPubkey,
46 #[builder(setter(into))]
48 pub market_token: StringPubkey,
49 #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
51 #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT)]
52 pub execution_lamports: u64,
53 #[cfg_attr(serde, serde(default))]
55 #[builder(default, setter(into))]
56 pub long_pay_token: Option<StringPubkey>,
57 #[cfg_attr(serde, serde(default))]
59 #[builder(default, setter(into))]
60 pub long_pay_token_account: Option<StringPubkey>,
61 #[cfg_attr(serde, serde(default))]
63 #[builder(default, setter(into))]
64 pub long_swap_path: Vec<StringPubkey>,
65 #[cfg_attr(serde, serde(default))]
67 #[builder(default, setter(into))]
68 pub short_pay_token: Option<StringPubkey>,
69 #[cfg_attr(serde, serde(default))]
71 #[builder(default, setter(into))]
72 pub short_pay_token_account: Option<StringPubkey>,
73 #[cfg_attr(serde, serde(default))]
75 #[builder(default, setter(into))]
76 pub short_swap_path: Vec<StringPubkey>,
77 #[cfg_attr(serde, serde(default))]
79 #[builder(default, setter(into))]
80 pub market_token_account: Option<StringPubkey>,
81 #[cfg_attr(serde, serde(default))]
83 #[builder(default)]
84 pub long_pay_amount: u64,
85 #[cfg_attr(serde, serde(default))]
87 #[builder(default)]
88 pub short_pay_amount: u64,
89 #[cfg_attr(serde, serde(default))]
91 #[builder(default)]
92 pub market_token_amount: u64,
93 #[cfg_attr(serde, serde(default))]
95 #[builder(default)]
96 pub min_market_token_amount: u64,
97 #[cfg_attr(serde, serde(default))]
99 #[builder(default)]
100 pub min_receive_amount: u64,
101 #[cfg_attr(serde, serde(default))]
103 #[builder(default)]
104 pub unwrap_native_on_receive: bool,
105 #[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#[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 #[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}