gmsol_sdk/builders/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::CreateWithdrawalParams,
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 utils::{generate_nonce, prepare_ata},
17 withdrawal::MIN_EXECUTION_LAMPORTS_FOR_WITHDRAWAL,
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 CreateWithdrawal {
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_WITHDRAWAL)]
47 pub execution_lamports: u64,
48 #[builder(setter(into))]
50 pub market_token: StringPubkey,
51 #[cfg_attr(serde, serde(default))]
53 #[builder(default, setter(into))]
54 pub market_token_account: Option<StringPubkey>,
55 #[cfg_attr(serde, serde(default))]
57 #[builder(default, setter(into))]
58 pub long_receive_token: Option<StringPubkey>,
59 #[cfg_attr(serde, serde(default))]
61 #[builder(default, setter(into))]
62 pub long_swap_path: Vec<StringPubkey>,
63 #[cfg_attr(serde, serde(default))]
65 #[builder(default, setter(into))]
66 pub short_receive_token: Option<StringPubkey>,
67 #[cfg_attr(serde, serde(default))]
69 #[builder(default, setter(into))]
70 pub short_swap_path: Vec<StringPubkey>,
71 #[cfg_attr(serde, serde(default))]
73 #[builder(default)]
74 pub market_token_amount: u64,
75 #[cfg_attr(serde, serde(default))]
77 #[builder(default)]
78 pub min_long_receive_amount: u64,
79 #[cfg_attr(serde, serde(default))]
81 #[builder(default)]
82 pub min_short_receive_amount: u64,
83 #[cfg_attr(serde, serde(default))]
85 #[builder(default)]
86 pub unwrap_native_on_receive: bool,
87 #[cfg_attr(serde, serde(default))]
89 #[builder(default)]
90 pub skip_long_receive_token_ata_creation: bool,
91 #[cfg_attr(serde, serde(default))]
93 #[builder(default)]
94 pub skip_short_receive_token_ata_creation: bool,
95}
96
97#[cfg(serde)]
98fn default_execution_lamports() -> u64 {
99 MIN_EXECUTION_LAMPORTS_FOR_WITHDRAWAL
100}
101
102impl StoreProgramIxBuilder for CreateWithdrawal {
103 fn store_program(&self) -> &StoreProgram {
104 &self.program
105 }
106}
107
108impl MarketTokenIxBuilder for CreateWithdrawal {
109 fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
110 &self.market_token
111 }
112}
113
114impl IntoAtomicGroup for CreateWithdrawal {
115 type Hint = CreateWithdrawalHint;
116
117 fn into_atomic_group(self, hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
118 if self.market_token_amount.is_zero() {
119 return Err(gmsol_solana_utils::Error::custom(
120 "invalid argument: empty withdrawal",
121 ));
122 }
123
124 let owner = self.payer.0;
125 let mut insts = AtomicGroup::new(&owner);
126
127 let receiver = self.receiver.as_deref().copied().unwrap_or(owner);
128 let nonce = self.nonce.unwrap_or_else(generate_nonce);
129 let withdrawal = self.program.find_withdrawal_address(&owner, &nonce);
130 let token_program_id = anchor_spl::token::ID;
131 let market_token = self.market_token.0;
132
133 let long_receive_token = self
134 .long_receive_token
135 .as_deref()
136 .unwrap_or(&hint.pool_tokens.long_token);
137 let short_receive_token = self
138 .short_receive_token
139 .as_deref()
140 .unwrap_or(&hint.pool_tokens.short_token);
141
142 let (long_receive_token_escrow, prepare) = prepare_ata(
143 &owner,
144 &withdrawal,
145 Some(long_receive_token),
146 &token_program_id,
147 )
148 .expect("must exist");
149 insts.add(prepare);
150
151 let (short_receive_token_escrow, prepare) = prepare_ata(
152 &owner,
153 &withdrawal,
154 Some(short_receive_token),
155 &token_program_id,
156 )
157 .expect("must exist");
158 insts.add(prepare);
159
160 let (market_token_escrow, prepare) =
161 prepare_ata(&owner, &withdrawal, Some(&market_token), &token_program_id)
162 .expect("must exist");
163 insts.add(prepare);
164
165 let market_token_account = self
166 .market_token_account
167 .as_deref()
168 .copied()
169 .unwrap_or_else(|| {
170 get_associated_token_address_with_program_id(
171 &owner,
172 &market_token,
173 &token_program_id,
174 )
175 });
176
177 let (_long_receive_token_ata, prepare) = prepare_ata(
178 &owner,
179 &receiver,
180 Some(long_receive_token),
181 &token_program_id,
182 )
183 .expect("must exist");
184 if !self.skip_long_receive_token_ata_creation {
185 insts.add(prepare);
186 }
187
188 let (_short_receive_token_ata, prepare) = prepare_ata(
189 &owner,
190 &receiver,
191 Some(short_receive_token),
192 &token_program_id,
193 )
194 .expect("must exist");
195 if !self.skip_short_receive_token_ata_creation {
196 insts.add(prepare);
197 }
198
199 let params = CreateWithdrawalParams {
200 execution_lamports: self.execution_lamports,
201 should_unwrap_native_token: self.unwrap_native_on_receive,
202 long_token_swap_path_length: self
203 .long_swap_path
204 .len()
205 .try_into()
206 .map_err(gmsol_solana_utils::Error::custom)?,
207 short_token_swap_path_length: self
208 .short_swap_path
209 .len()
210 .try_into()
211 .map_err(gmsol_solana_utils::Error::custom)?,
212 market_token_amount: self.market_token_amount,
213 min_long_token_amount: self.min_long_receive_amount,
214 min_short_token_amount: self.min_short_receive_amount,
215 };
216
217 let create = self
218 .program
219 .anchor_instruction(args::CreateWithdrawal {
220 nonce: nonce.to_bytes(),
221 params,
222 })
223 .anchor_accounts(
224 accounts::CreateWithdrawal {
225 owner,
226 receiver,
227 store: self.program.store.0,
228 market: self.program.find_market_address(&market_token),
229 withdrawal,
230 market_token,
231 market_token_escrow,
232 final_long_token: *long_receive_token,
233 final_short_token: *short_receive_token,
234 final_long_token_escrow: long_receive_token_escrow,
235 final_short_token_escrow: short_receive_token_escrow,
236 market_token_source: market_token_account,
237 system_program: system_program::ID,
238 token_program: token_program_id,
239 associated_token_program: associated_token::ID,
240 },
241 true,
242 )
243 .accounts(
244 self.long_swap_path
245 .iter()
246 .chain(self.short_swap_path.iter())
247 .map(|token| AccountMeta {
248 pubkey: self.program.find_market_address(token),
249 is_signer: false,
250 is_writable: false,
251 })
252 .collect::<Vec<_>>(),
253 )
254 .build();
255 insts.add(create);
256
257 Ok(insts)
258 }
259}
260
261#[cfg_attr(js, derive(tsify_next::Tsify))]
263#[cfg_attr(js, tsify(from_wasm_abi))]
264#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
265#[derive(Debug, Clone, TypedBuilder)]
266pub struct CreateWithdrawalHint {
267 #[builder(setter(into))]
269 pub pool_tokens: PoolTokenHint,
270}
271
272impl FromRpcClientWith<CreateWithdrawal> for CreateWithdrawalHint {
273 async fn from_rpc_client_with<'a>(
274 builder: &'a CreateWithdrawal,
275 client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
276 ) -> gmsol_solana_utils::Result<Self> {
277 let pool_tokens = PoolTokenHint::from_rpc_client_with(builder, client).await?;
278 Ok(Self { pool_tokens })
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 #[cfg(not(target_arch = "wasm32"))]
285 use tokio::test as async_test;
286
287 #[cfg(target_arch = "wasm32")]
288 use wasm_bindgen_test::wasm_bindgen_test as async_test;
289
290 use gmsol_solana_utils::{
291 client_traits::GenericRpcClient, cluster::Cluster, transaction_builder::default_before_sign,
292 };
293 use solana_sdk::pubkey::Pubkey;
294
295 use super::*;
296
297 #[test]
298 fn create_withdrawal() -> crate::Result<()> {
299 let long_token = Pubkey::new_unique();
300 let short_token = Pubkey::new_unique();
301 CreateWithdrawal::builder()
302 .payer(Pubkey::new_unique())
303 .long_swap_path([Pubkey::new_unique().into()])
304 .market_token_amount(1_000_000_000)
305 .long_receive_token(Some(Pubkey::new_unique().into()))
306 .market_token(Pubkey::new_unique())
307 .unwrap_native_on_receive(true)
308 .build()
309 .into_atomic_group(
310 &CreateWithdrawalHint::builder()
311 .pool_tokens(
312 PoolTokenHint::builder()
313 .long_token(long_token)
314 .short_token(short_token)
315 .build(),
316 )
317 .build(),
318 )?
319 .partially_signed_transaction_with_blockhash_and_options(
320 Default::default(),
321 Default::default(),
322 None,
323 default_before_sign,
324 )?;
325 Ok(())
326 }
327
328 #[async_test]
329 async fn create_withdrawal_with_rpc() -> crate::Result<()> {
330 let market_token: Pubkey = "5sdFW7wrKsxxYHMXoqDmNHkGyCWsbLEFb1x1gzBBm4Hx".parse()?;
331 let wsol: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
332
333 let cluster = Cluster::Devnet;
334 let client = GenericRpcClient::new(cluster.url());
335
336 CreateWithdrawal::builder()
337 .payer(Pubkey::new_unique())
338 .short_swap_path([Pubkey::new_unique().into()])
339 .short_receive_token(Some(wsol.into()))
340 .market_token_amount(1_000_000_000)
341 .market_token(market_token)
342 .unwrap_native_on_receive(true)
343 .build()
344 .into_atomic_group_with_rpc_client(&client)
345 .await?
346 .partially_signed_transaction_with_blockhash_and_options(
347 Default::default(),
348 Default::default(),
349 None,
350 default_before_sign,
351 )?;
352
353 Ok(())
354 }
355}