Skip to main content

gmsol_sdk/builders/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::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/// Builder for the `create_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 CreateWithdrawal {
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 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_WITHDRAWAL)]
47    pub execution_lamports: u64,
48    /// The market token of the market in which the withdrawal will be created.
49    #[builder(setter(into))]
50    pub market_token: StringPubkey,
51    /// Market token account.
52    #[cfg_attr(serde, serde(default))]
53    #[builder(default, setter(into))]
54    pub market_token_account: Option<StringPubkey>,
55    /// Long receive token.
56    #[cfg_attr(serde, serde(default))]
57    #[builder(default, setter(into))]
58    pub long_receive_token: Option<StringPubkey>,
59    /// Swap path for long receive token.
60    #[cfg_attr(serde, serde(default))]
61    #[builder(default, setter(into))]
62    pub long_swap_path: Vec<StringPubkey>,
63    /// Short receive token.
64    #[cfg_attr(serde, serde(default))]
65    #[builder(default, setter(into))]
66    pub short_receive_token: Option<StringPubkey>,
67    /// Swap path for short receive token.
68    #[cfg_attr(serde, serde(default))]
69    #[builder(default, setter(into))]
70    pub short_swap_path: Vec<StringPubkey>,
71    /// Market token amount.
72    #[cfg_attr(serde, serde(default))]
73    #[builder(default)]
74    pub market_token_amount: u64,
75    /// Minimum amount of long receive tokens.
76    #[cfg_attr(serde, serde(default))]
77    #[builder(default)]
78    pub min_long_receive_amount: u64,
79    /// Minimum amount of short receive tokens.
80    #[cfg_attr(serde, serde(default))]
81    #[builder(default)]
82    pub min_short_receive_amount: u64,
83    /// Whether to unwrap the native token when receiving (e.g., convert WSOL to SOL).
84    #[cfg_attr(serde, serde(default))]
85    #[builder(default)]
86    pub unwrap_native_on_receive: bool,
87    /// Whether to skip the creation of long receive token ATA.
88    #[cfg_attr(serde, serde(default))]
89    #[builder(default)]
90    pub skip_long_receive_token_ata_creation: bool,
91    /// Whether to skip the creation of short receive token ATA.
92    #[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/// Hint for [`CreateWithdrawal`].
262#[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    /// Pool tokens.
268    #[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}