gmsol_sdk/builders/deposit/
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::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#[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 #[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 #[builder(setter(into))]
46 pub market_token: StringPubkey,
47 #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
49 #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_DEPOSIT)]
50 pub execution_lamports: u64,
51 #[cfg_attr(serde, serde(default))]
53 #[builder(default, setter(into))]
54 pub long_pay_token: Option<StringPubkey>,
55 #[cfg_attr(serde, serde(default))]
57 #[builder(default, setter(into))]
58 pub long_pay_token_account: 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_pay_token: Option<StringPubkey>,
67 #[cfg_attr(serde, serde(default))]
69 #[builder(default, setter(into))]
70 pub short_pay_token_account: Option<StringPubkey>,
71 #[cfg_attr(serde, serde(default))]
73 #[builder(default, setter(into))]
74 pub short_swap_path: Vec<StringPubkey>,
75 #[cfg_attr(serde, serde(default))]
77 #[builder(default)]
78 pub long_pay_amount: u64,
79 #[cfg_attr(serde, serde(default))]
81 #[builder(default)]
82 pub short_pay_amount: u64,
83 #[cfg_attr(serde, serde(default))]
85 #[builder(default)]
86 pub min_receive_amount: u64,
87 #[cfg_attr(serde, serde(default))]
89 #[builder(default)]
90 pub unwrap_native_on_receive: bool,
91 #[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#[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 #[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}