Skip to main content

gmsol_sdk/builders/deposit/
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::CreateDepositParams,
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        deposit::MIN_EXECUTION_LAMPORTS_FOR_DEPOSIT,
17        utils::{generate_nonce, prepare_ata},
18        MarketTokenIxBuilder, NonceBytes, PoolTokenHint, StoreProgram, StoreProgramIxBuilder,
19    },
20    serde::StringPubkey,
21};
22
23/// Builder for the `create_deposit` 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 CreateDeposit {
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 deposit.
41    #[cfg_attr(serde, serde(default))]
42    #[builder(default, setter(strip_option, into))]
43    pub nonce: Option<NonceBytes>,
44    /// The market token of the market in which the deposit will be created.
45    #[builder(setter(into))]
46    pub market_token: StringPubkey,
47    /// Execution fee paid to the keeper in lamports.
48    #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
49    #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_DEPOSIT)]
50    pub execution_lamports: u64,
51    /// Long pay token.
52    #[cfg_attr(serde, serde(default))]
53    #[builder(default, setter(into))]
54    pub long_pay_token: Option<StringPubkey>,
55    /// Long pay token account.
56    #[cfg_attr(serde, serde(default))]
57    #[builder(default, setter(into))]
58    pub long_pay_token_account: Option<StringPubkey>,
59    /// Swap path for long pay token.
60    #[cfg_attr(serde, serde(default))]
61    #[builder(default, setter(into))]
62    pub long_swap_path: Vec<StringPubkey>,
63    /// Short pay token.
64    #[cfg_attr(serde, serde(default))]
65    #[builder(default, setter(into))]
66    pub short_pay_token: Option<StringPubkey>,
67    /// Short pay token account.
68    #[cfg_attr(serde, serde(default))]
69    #[builder(default, setter(into))]
70    pub short_pay_token_account: Option<StringPubkey>,
71    /// Swap path for short pay token.
72    #[cfg_attr(serde, serde(default))]
73    #[builder(default, setter(into))]
74    pub short_swap_path: Vec<StringPubkey>,
75    /// Long pay token amount.
76    #[cfg_attr(serde, serde(default))]
77    #[builder(default)]
78    pub long_pay_amount: u64,
79    /// Short pay token amount.
80    #[cfg_attr(serde, serde(default))]
81    #[builder(default)]
82    pub short_pay_amount: u64,
83    /// Minimum amount of output market tokens.
84    #[cfg_attr(serde, serde(default))]
85    #[builder(default)]
86    pub min_receive_amount: u64,
87    /// Whether to unwrap the native token when receiving (e.g., convert WSOL to SOL).
88    #[cfg_attr(serde, serde(default))]
89    #[builder(default)]
90    pub unwrap_native_on_receive: bool,
91    /// Whether to skip the creation of market token ATA.
92    #[cfg_attr(serde, serde(default))]
93    #[builder(default)]
94    pub skip_market_token_ata_creation: bool,
95}
96
97#[cfg(serde)]
98fn default_execution_lamports() -> u64 {
99    MIN_EXECUTION_LAMPORTS_FOR_DEPOSIT
100}
101
102impl StoreProgramIxBuilder for CreateDeposit {
103    fn store_program(&self) -> &StoreProgram {
104        &self.program
105    }
106}
107
108impl MarketTokenIxBuilder for CreateDeposit {
109    fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
110        &self.market_token
111    }
112}
113
114impl IntoAtomicGroup for CreateDeposit {
115    type Hint = CreateDepositHint;
116
117    fn into_atomic_group(self, hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
118        if self.long_pay_amount.is_zero() && self.short_pay_amount.is_zero() {
119            return Err(gmsol_solana_utils::Error::custom(
120                "invalid argument: empty deposit",
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 deposit = self.program.find_deposit_address(&owner, &nonce);
130        let token_program_id = anchor_spl::token::ID;
131        let market_token = self.market_token.0;
132
133        let long_pay_token = (!self.long_pay_amount.is_zero()).then(|| {
134            self.long_pay_token
135                .as_deref()
136                .unwrap_or(&hint.pool_tokens.long_token)
137        });
138        let long_pay_token_account = long_pay_token.as_ref().map(|token| {
139            self.long_pay_token_account
140                .as_deref()
141                .copied()
142                .unwrap_or_else(|| {
143                    get_associated_token_address_with_program_id(&owner, token, &token_program_id)
144                })
145        });
146        let short_pay_token = (!self.short_pay_amount.is_zero()).then(|| {
147            self.short_pay_token
148                .as_deref()
149                .unwrap_or(&hint.pool_tokens.short_token)
150        });
151        let short_pay_token_account = short_pay_token.as_ref().map(|token| {
152            self.short_pay_token_account
153                .as_deref()
154                .copied()
155                .unwrap_or_else(|| {
156                    get_associated_token_address_with_program_id(&owner, token, &token_program_id)
157                })
158        });
159
160        let (long_pay_token_escrow, prepare) =
161            prepare_ata(&owner, &deposit, long_pay_token, &token_program_id).unzip();
162        insts.extend(prepare);
163
164        let (short_pay_token_escrow, prepare) =
165            prepare_ata(&owner, &deposit, short_pay_token, &token_program_id).unzip();
166        insts.extend(prepare);
167
168        let (market_token_escrow, prepare) =
169            prepare_ata(&owner, &deposit, Some(&market_token), &token_program_id)
170                .expect("must exist");
171        insts.add(prepare);
172
173        let (market_token_ata, prepare) =
174            prepare_ata(&owner, &receiver, Some(&market_token), &token_program_id)
175                .expect("must exist");
176        if !self.skip_market_token_ata_creation {
177            insts.add(prepare);
178        }
179
180        let params = CreateDepositParams {
181            execution_lamports: self.execution_lamports,
182            long_token_swap_length: self
183                .long_swap_path
184                .len()
185                .try_into()
186                .map_err(gmsol_solana_utils::Error::custom)?,
187            short_token_swap_length: self
188                .short_swap_path
189                .len()
190                .try_into()
191                .map_err(gmsol_solana_utils::Error::custom)?,
192            initial_long_token_amount: self.long_pay_amount,
193            initial_short_token_amount: self.short_pay_amount,
194            min_market_token_amount: self.min_receive_amount,
195            should_unwrap_native_token: self.unwrap_native_on_receive,
196        };
197
198        let create = self
199            .program
200            .anchor_instruction(args::CreateDeposit {
201                nonce: nonce.to_bytes(),
202                params,
203            })
204            .anchor_accounts(
205                accounts::CreateDeposit {
206                    owner,
207                    receiver,
208                    store: self.program.store.0,
209                    market: self.program.find_market_address(&market_token),
210                    deposit,
211                    market_token,
212                    initial_long_token: long_pay_token.copied(),
213                    initial_short_token: short_pay_token.copied(),
214                    market_token_escrow,
215                    initial_long_token_escrow: long_pay_token_escrow,
216                    initial_short_token_escrow: short_pay_token_escrow,
217                    market_token_ata,
218                    initial_long_token_source: long_pay_token_account,
219                    initial_short_token_source: short_pay_token_account,
220                    system_program: system_program::ID,
221                    token_program: token_program_id,
222                    associated_token_program: associated_token::ID,
223                },
224                true,
225            )
226            .accounts(
227                self.long_swap_path
228                    .iter()
229                    .chain(self.short_swap_path.iter())
230                    .map(|token| AccountMeta {
231                        pubkey: self.program.find_market_address(token),
232                        is_signer: false,
233                        is_writable: false,
234                    })
235                    .collect::<Vec<_>>(),
236            )
237            .build();
238        insts.add(create);
239
240        Ok(insts)
241    }
242}
243
244/// Hint for [`CreateDeposit`].
245#[cfg_attr(js, derive(tsify_next::Tsify))]
246#[cfg_attr(js, tsify(from_wasm_abi))]
247#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
248#[derive(Debug, Clone, TypedBuilder)]
249pub struct CreateDepositHint {
250    /// Pool tokens.
251    #[builder(setter(into))]
252    pub pool_tokens: PoolTokenHint,
253}
254
255impl FromRpcClientWith<CreateDeposit> for CreateDepositHint {
256    async fn from_rpc_client_with<'a>(
257        builder: &'a CreateDeposit,
258        client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
259    ) -> gmsol_solana_utils::Result<Self> {
260        let pool_tokens = PoolTokenHint::from_rpc_client_with(builder, client).await?;
261        Ok(Self { pool_tokens })
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    #[cfg(not(target_arch = "wasm32"))]
268    use tokio::test as async_test;
269
270    #[cfg(target_arch = "wasm32")]
271    use wasm_bindgen_test::wasm_bindgen_test as async_test;
272
273    use gmsol_solana_utils::{
274        client_traits::GenericRpcClient, cluster::Cluster, transaction_builder::default_before_sign,
275    };
276    use solana_sdk::pubkey::Pubkey;
277
278    use super::*;
279
280    #[test]
281    fn create_deposit() -> crate::Result<()> {
282        let long_token = Pubkey::new_unique();
283        let short_token = Pubkey::new_unique();
284        CreateDeposit::builder()
285            .payer(Pubkey::new_unique())
286            .long_swap_path([Pubkey::new_unique().into()])
287            .long_pay_amount(1_000_000_000)
288            .long_pay_token(Some(Pubkey::new_unique().into()))
289            .market_token(Pubkey::new_unique())
290            .unwrap_native_on_receive(true)
291            .build()
292            .into_atomic_group(
293                &CreateDepositHint::builder()
294                    .pool_tokens(
295                        PoolTokenHint::builder()
296                            .long_token(long_token)
297                            .short_token(short_token)
298                            .build(),
299                    )
300                    .build(),
301            )?
302            .partially_signed_transaction_with_blockhash_and_options(
303                Default::default(),
304                Default::default(),
305                None,
306                default_before_sign,
307            )?;
308        Ok(())
309    }
310
311    #[async_test]
312    async fn create_deposit_with_rpc() -> crate::Result<()> {
313        let market_token: Pubkey = "5sdFW7wrKsxxYHMXoqDmNHkGyCWsbLEFb1x1gzBBm4Hx".parse()?;
314        let wsol: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
315
316        let cluster = Cluster::Devnet;
317        let client = GenericRpcClient::new(cluster.url());
318
319        CreateDeposit::builder()
320            .payer(Pubkey::new_unique())
321            .short_swap_path([Pubkey::new_unique().into()])
322            .short_pay_amount(1_000_000_000)
323            .short_pay_token(Some(wsol.into()))
324            .market_token(market_token)
325            .unwrap_native_on_receive(true)
326            .build()
327            .into_atomic_group_with_rpc_client(&client)
328            .await?
329            .partially_signed_transaction_with_blockhash_and_options(
330                Default::default(),
331                Default::default(),
332                None,
333                default_before_sign,
334            )?;
335
336        Ok(())
337    }
338}