Skip to main content

gmsol_sdk/builders/shift/
create.rs

1use 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/// Builder for the `create_shift` instruction.
21#[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    /// Program.
27    #[cfg_attr(serde, serde(default))]
28    #[builder(default)]
29    pub program: StoreProgram,
30    /// Payer (a.k.a. owner).
31    #[builder(setter(into))]
32    pub payer: StringPubkey,
33    /// Reciever.
34    #[cfg_attr(serde, serde(default))]
35    #[builder(default, setter(strip_option, into))]
36    pub receiver: Option<StringPubkey>,
37    /// Nonce for the shift.
38    #[cfg_attr(serde, serde(default))]
39    #[builder(default, setter(strip_option, into))]
40    pub nonce: Option<NonceBytes>,
41    /// The from-market token.
42    #[builder(setter(into))]
43    pub from_market_token: StringPubkey,
44    /// From-market token account.
45    #[cfg_attr(serde, serde(default))]
46    #[builder(default, setter(into))]
47    pub from_market_token_account: Option<StringPubkey>,
48    /// The to-market token.
49    #[builder(setter(into))]
50    pub to_market_token: StringPubkey,
51    /// Execution fee paid to the keeper in lamports.
52    #[cfg_attr(serde, serde(default = "default_execution_lamports"))]
53    #[builder(default = MIN_EXECUTION_LAMPORTS_FOR_SHIFT)]
54    pub execution_lamports: u64,
55    /// From-market token amount to pay.
56    #[cfg_attr(serde, serde(default))]
57    #[builder(default)]
58    pub from_market_token_amount: u64,
59    /// Minimum to-market token amount to receive.
60    #[cfg_attr(serde, serde(default))]
61    #[builder(default)]
62    pub min_to_market_token_amount: u64,
63    /// Whether to skip the creation of to-market token ATA.
64    #[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}