gmsol_sdk/builders/shift/
create.rs1use 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::CreateShiftParams,
6};
7use gmsol_solana_utils::{AtomicGroup, IntoAtomicGroup, ProgramExt};
8use solana_sdk::system_program;
9use typed_builder::TypedBuilder;
10
11use crate::{
12 builders::{
13 shift::MIN_EXECUTION_LAMPORTS_FOR_SHIFT,
14 utils::{generate_nonce, prepare_ata},
15 MarketTokenIxBuilder, NonceBytes, StoreProgram, StoreProgramIxBuilder,
16 },
17 serde::StringPubkey,
18};
19
20#[cfg_attr(js, derive(tsify_next::Tsify))]
22#[cfg_attr(js, tsify(from_wasm_abi))]
23#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
24#[derive(Debug, Clone, TypedBuilder)]
25pub struct CreateShift {
26 #[cfg_attr(serde, serde(default))]
28 #[builder(default)]
29 pub program: StoreProgram,
30 #[builder(setter(into))]
32 pub payer: StringPubkey,
33 #[cfg_attr(serde, serde(default))]
35 #[builder(default, setter(strip_option, into))]
36 pub receiver: Option<StringPubkey>,
37 #[cfg_attr(serde, serde(default))]
39 #[builder(default, setter(strip_option, into))]
40 pub nonce: Option<NonceBytes>,
41 #[builder(setter(into))]
43 pub from_market_token: StringPubkey,
44 #[cfg_attr(serde, serde(default))]
46 #[builder(default, setter(into))]
47 pub from_market_token_account: Option<StringPubkey>,
48 #[builder(setter(into))]
50 pub to_market_token: StringPubkey,
51 #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
53 #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_SHIFT)]
54 pub execution_lamports: u64,
55 #[cfg_attr(serde, serde(default))]
57 #[builder(default)]
58 pub from_market_token_amount: u64,
59 #[cfg_attr(serde, serde(default))]
61 #[builder(default)]
62 pub min_to_market_token_amount: u64,
63 #[cfg_attr(serde, serde(default))]
65 #[builder(default)]
66 pub skip_to_market_token_ata_creation: bool,
67}
68
69#[cfg(serde)]
70fn default_execution_lamports() -> u64 {
71 MIN_EXECUTION_LAMPORTS_FOR_SHIFT
72}
73
74impl StoreProgramIxBuilder for CreateShift {
75 fn store_program(&self) -> &StoreProgram {
76 &self.program
77 }
78}
79
80impl MarketTokenIxBuilder for CreateShift {
81 fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
82 &self.from_market_token
83 }
84}
85
86impl IntoAtomicGroup for CreateShift {
87 type Hint = ();
88
89 fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
90 if self.from_market_token_amount.is_zero() {
91 return Err(gmsol_solana_utils::Error::custom(
92 "invalid argument: empty shift",
93 ));
94 }
95
96 let owner = self.payer.0;
97 let mut insts = AtomicGroup::new(&owner);
98
99 let receiver = self.receiver.as_deref().copied().unwrap_or(owner);
100 let nonce = self.nonce.unwrap_or_else(generate_nonce);
101 let shift = self.program.find_shift_address(&owner, &nonce);
102 let token_program_id = anchor_spl::token::ID;
103 let from_market_token = self.from_market_token.0;
104 let to_market_token = self.to_market_token.0;
105
106 let from_market_token_account = self
107 .from_market_token_account
108 .as_deref()
109 .copied()
110 .unwrap_or_else(|| {
111 get_associated_token_address_with_program_id(
112 &owner,
113 &from_market_token,
114 &token_program_id,
115 )
116 });
117
118 let (from_market_token_escrow, prepare) =
119 prepare_ata(&owner, &shift, Some(&from_market_token), &token_program_id)
120 .expect("must exist");
121 insts.add(prepare);
122
123 let (to_market_token_escrow, prepare) =
124 prepare_ata(&owner, &shift, Some(&to_market_token), &token_program_id)
125 .expect("must exist");
126 insts.add(prepare);
127
128 let (to_market_token_ata, prepare) =
129 prepare_ata(&owner, &receiver, Some(&to_market_token), &token_program_id)
130 .expect("must exist");
131 if !self.skip_to_market_token_ata_creation {
132 insts.add(prepare);
133 }
134
135 let params = CreateShiftParams {
136 execution_lamports: self.execution_lamports,
137 from_market_token_amount: self.from_market_token_amount,
138 min_to_market_token_amount: self.min_to_market_token_amount,
139 };
140
141 let create = self
142 .program
143 .anchor_instruction(args::CreateShift {
144 nonce: nonce.to_bytes(),
145 params,
146 })
147 .anchor_accounts(
148 accounts::CreateShift {
149 owner,
150 receiver,
151 store: self.program.store.0,
152 from_market: self.program.find_market_address(&from_market_token),
153 to_market: self.program.find_market_address(&to_market_token),
154 shift,
155 from_market_token,
156 to_market_token,
157 from_market_token_escrow,
158 to_market_token_escrow,
159 from_market_token_source: from_market_token_account,
160 to_market_token_ata,
161 system_program: system_program::ID,
162 token_program: token_program_id,
163 associated_token_program: associated_token::ID,
164 },
165 false,
166 )
167 .build();
168 insts.add(create);
169
170 Ok(insts)
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use gmsol_solana_utils::transaction_builder::default_before_sign;
177 use solana_sdk::pubkey::Pubkey;
178
179 use super::*;
180
181 #[test]
182 fn create_shift() -> crate::Result<()> {
183 CreateShift::builder()
184 .payer(Pubkey::new_unique())
185 .from_market_token(Pubkey::new_unique())
186 .to_market_token(Pubkey::new_unique())
187 .from_market_token_amount(1_000_000_000)
188 .build()
189 .into_atomic_group(&())?
190 .partially_signed_transaction_with_blockhash_and_options(
191 Default::default(),
192 Default::default(),
193 None,
194 default_before_sign,
195 )?;
196 Ok(())
197 }
198}