gmsol_sdk/client/ops/
liquidity_provider.rs

1use std::ops::Deref;
2
3use gmsol_solana_utils::{
4    client_traits::FromRpcClientWith,
5    make_bundle_builder::{MakeBundleBuilder, SetExecutionFee},
6    transaction_builder::TransactionBuilder,
7    IntoAtomicGroup,
8};
9use gmsol_utils::oracle::PriceProviderKind;
10use solana_sdk::{pubkey::Pubkey, signer::Signer};
11
12use crate::{
13    builders::{
14        liquidity_provider::{
15            AcceptAuthority, ClaimGtReward, CreateLpTokenController, DisableLpTokenController,
16            GtRewardCalculationParams, InitializeLp, LpPositionQueryParams, SetClaimEnabled,
17            SetPricingStaleness, StakeLpToken, StakeLpTokenHint, StakeLpTokenParams,
18            TransferAuthority, UnstakeLpToken, UnstakeLpTokenParams, UpdateApyGradientRange,
19            UpdateApyGradientSparse, UpdateMinStakeValue,
20        },
21        StoreProgram,
22    },
23    client::pull_oracle::{FeedIds, PullOraclePriceConsumer},
24    utils::token_map::FeedAddressMap,
25};
26
27// ============================================================================
28// Constants
29// ============================================================================
30
31/// Compute budget for LP token staking operations
32const STAKE_LP_TOKEN_COMPUTE_BUDGET: u32 = 800_000;
33
34/// Operations for liquidity-provider program.
35pub trait LiquidityProviderOps<C> {
36    /// Initialize LP staking program.
37    fn initialize_lp(
38        &self,
39        min_stake_value: u128,
40        initial_apy: u128,
41    ) -> crate::Result<TransactionBuilder<'_, C>>;
42
43    /// Create LP token controller for a specific token mint.
44    fn create_lp_token_controller(
45        &self,
46        lp_token_mint: &Pubkey,
47        controller_index: u64,
48    ) -> crate::Result<TransactionBuilder<'_, C>>;
49
50    /// Disable LP token controller for a specific token mint.
51    fn disable_lp_token_controller(
52        &self,
53        store: &Pubkey,
54        lp_token_mint: &Pubkey,
55        controller_index: u64,
56        controller_address: Option<Pubkey>,
57    ) -> crate::Result<TransactionBuilder<'_, C>>;
58
59    /// Unstake LP token.
60    fn unstake_lp_token(
61        &self,
62        params: UnstakeLpTokenParams<'_>,
63    ) -> crate::Result<TransactionBuilder<'_, C>>;
64
65    /// Stake LP token.
66    fn stake_lp_token(&self, params: StakeLpTokenParams<'_>) -> StakeLpTokenBuilder<'_, C>;
67
68    /// Calculate GT reward for a position.
69    fn calculate_gt_reward(
70        &self,
71        params: GtRewardCalculationParams<'_>,
72    ) -> impl std::future::Future<Output = crate::Result<u128>>;
73
74    /// Claim GT rewards for a position.
75    fn claim_gt_reward(
76        &self,
77        store: &Pubkey,
78        lp_token_mint: &Pubkey,
79        position_id: u64,
80        controller_index: u64,
81        controller_address: Option<Pubkey>,
82    ) -> crate::Result<TransactionBuilder<'_, C>>;
83
84    /// Transfer LP program authority to a new authority.
85    fn transfer_lp_authority(
86        &self,
87        new_authority: &Pubkey,
88    ) -> crate::Result<TransactionBuilder<'_, C>>;
89
90    /// Accept LP program authority transfer.
91    fn accept_lp_authority(&self) -> crate::Result<TransactionBuilder<'_, C>>;
92
93    /// Set whether claiming GT at any time is allowed.
94    fn set_claim_enabled(&self, enabled: bool) -> crate::Result<TransactionBuilder<'_, C>>;
95
96    /// Set pricing staleness configuration.
97    fn set_pricing_staleness(
98        &self,
99        staleness_seconds: u32,
100    ) -> crate::Result<TransactionBuilder<'_, C>>;
101
102    /// Update APY gradient with sparse entries.
103    fn update_apy_gradient_sparse(
104        &self,
105        bucket_indices: Vec<u8>,
106        apy_values: Vec<u128>,
107    ) -> crate::Result<TransactionBuilder<'_, C>>;
108
109    /// Update APY gradient for a contiguous range.
110    fn update_apy_gradient_range(
111        &self,
112        start_bucket: u8,
113        end_bucket: u8,
114        apy_values: Vec<u128>,
115    ) -> crate::Result<TransactionBuilder<'_, C>>;
116
117    /// Update minimum stake value.
118    fn update_min_stake_value(
119        &self,
120        new_min_stake_value: u128,
121    ) -> crate::Result<TransactionBuilder<'_, C>>;
122
123    /// Query all LP staking positions for a specific owner.
124    fn get_lp_positions(
125        &self,
126        store: &Pubkey,
127        owner: &Pubkey,
128    ) -> impl std::future::Future<
129        Output = crate::Result<Vec<crate::serde::serde_lp_position::SerdeLpStakingPosition>>,
130    >;
131
132    /// Query a specific LP staking position.
133    fn get_lp_position(
134        &self,
135        params: LpPositionQueryParams<'_>,
136    ) -> impl std::future::Future<
137        Output = crate::Result<Option<crate::serde::serde_lp_position::SerdeLpStakingPosition>>,
138    >;
139
140    /// Query all LP staking positions for the current wallet.
141    fn get_my_lp_positions(
142        &self,
143        store: &Pubkey,
144    ) -> impl std::future::Future<
145        Output = crate::Result<Vec<crate::serde::serde_lp_position::SerdeLpStakingPosition>>,
146    >;
147
148    /// Query LP controllers for a specific token mint.
149    fn get_lp_controllers(
150        &self,
151        lp_token_mint: &Pubkey,
152    ) -> impl std::future::Future<
153        Output = crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>>,
154    >;
155
156    /// Query all LP controllers.
157    fn get_all_lp_controllers(
158        &self,
159    ) -> impl std::future::Future<
160        Output = crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>>,
161    >;
162
163    /// Query LP Global State information (authority, APY gradient, min stake value).
164    fn get_lp_global_state(
165        &self,
166    ) -> impl std::future::Future<
167        Output = crate::Result<crate::serde::serde_lp_global_state::SerdeLpGlobalState>,
168    >;
169}
170
171impl<C: Deref<Target = impl Signer> + Clone> LiquidityProviderOps<C> for crate::Client<C> {
172    fn initialize_lp(
173        &self,
174        min_stake_value: u128,
175        initial_apy: u128,
176    ) -> crate::Result<TransactionBuilder<'_, C>> {
177        let builder = InitializeLp::builder()
178            .payer(self.payer())
179            .lp_program(self.lp_program_for_builders().clone())
180            .min_stake_value(min_stake_value)
181            .initial_apy(initial_apy)
182            .build();
183
184        let ag = builder.into_atomic_group(&())?;
185        Ok(self.store_transaction().pre_atomic_group(ag, true))
186    }
187
188    fn create_lp_token_controller(
189        &self,
190        lp_token_mint: &Pubkey,
191        controller_index: u64,
192    ) -> crate::Result<TransactionBuilder<'_, C>> {
193        let builder = CreateLpTokenController::builder()
194            .authority(self.payer())
195            .lp_program(self.lp_program_for_builders().clone())
196            .lp_token_mint(*lp_token_mint)
197            .controller_index(controller_index)
198            .build();
199
200        let ag = builder.into_atomic_group(&())?;
201        Ok(self.store_transaction().pre_atomic_group(ag, true))
202    }
203
204    fn disable_lp_token_controller(
205        &self,
206        store: &Pubkey,
207        lp_token_mint: &Pubkey,
208        controller_index: u64,
209        controller_address: Option<Pubkey>,
210    ) -> crate::Result<TransactionBuilder<'_, C>> {
211        let builder = DisableLpTokenController::builder()
212            .authority(self.payer())
213            .store_program(self.store_program_for_builders(store))
214            .lp_program(self.lp_program_for_builders().clone())
215            .lp_token_mint(*lp_token_mint)
216            .controller_index(controller_index)
217            .controller_address(controller_address.map(|addr| addr.into()))
218            .build();
219
220        let ag = builder.into_atomic_group(&())?;
221        Ok(self.store_transaction().pre_atomic_group(ag, true))
222    }
223
224    fn unstake_lp_token(
225        &self,
226        params: UnstakeLpTokenParams<'_>,
227    ) -> crate::Result<TransactionBuilder<'_, C>> {
228        let builder = UnstakeLpToken::builder()
229            .payer(self.payer())
230            .store_program(self.store_program_for_builders(params.store))
231            .lp_program(self.lp_program_for_builders().clone())
232            .lp_token_kind(params.lp_token_kind)
233            .lp_token_mint(*params.lp_token_mint)
234            .position_id(params.position_id)
235            .unstake_amount(params.unstake_amount)
236            .controller_index(params.controller_index)
237            .controller_address(params.controller_address.map(|addr| addr.into()))
238            .build();
239
240        let ag = builder.into_atomic_group(&())?;
241        Ok(self.store_transaction().pre_atomic_group(ag, true))
242    }
243
244    fn stake_lp_token(&self, params: StakeLpTokenParams<'_>) -> StakeLpTokenBuilder<'_, C> {
245        StakeLpTokenBuilder {
246            client: self,
247            builder: StakeLpToken::builder()
248                .payer(self.payer())
249                .amount(params.amount)
250                .oracle(*params.oracle)
251                .lp_token_kind(params.lp_token_kind)
252                .lp_token_mint(*params.lp_token_mint)
253                .lp_program(self.lp_program_for_builders().clone())
254                .store_program(
255                    StoreProgram::builder()
256                        .id(*self.store_program_id())
257                        .store(*params.store)
258                        .build(),
259                )
260                .controller_index(params.controller_index)
261                .controller_address(params.controller_address.map(|addr| addr.into()))
262                .build(),
263            hint: None,
264        }
265    }
266
267    async fn calculate_gt_reward(
268        &self,
269        params: GtRewardCalculationParams<'_>,
270    ) -> crate::Result<u128> {
271        let lp_program = self.lp_program_for_builders();
272
273        lp_program.calculate_gt_reward(self.rpc(), &params).await
274    }
275
276    fn claim_gt_reward(
277        &self,
278        store: &Pubkey,
279        lp_token_mint: &Pubkey,
280        position_id: u64,
281        controller_index: u64,
282        controller_address: Option<Pubkey>,
283    ) -> crate::Result<TransactionBuilder<'_, C>> {
284        let builder = ClaimGtReward::builder()
285            .owner(self.payer())
286            .store_program(self.store_program_for_builders(store))
287            .lp_program(self.lp_program_for_builders().clone())
288            .lp_token_mint(*lp_token_mint)
289            .position_id(position_id)
290            .controller_index(controller_index)
291            .controller_address(controller_address.map(|addr| addr.into()))
292            .build();
293
294        let ag = builder.into_atomic_group(&())?;
295        Ok(self.store_transaction().pre_atomic_group(ag, true))
296    }
297
298    fn transfer_lp_authority(
299        &self,
300        new_authority: &Pubkey,
301    ) -> crate::Result<TransactionBuilder<'_, C>> {
302        let builder = TransferAuthority::builder()
303            .authority(self.payer())
304            .lp_program(self.lp_program_for_builders().clone())
305            .new_authority(*new_authority)
306            .build();
307
308        let ag = builder.into_atomic_group(&())?;
309        Ok(self.store_transaction().pre_atomic_group(ag, true))
310    }
311
312    fn accept_lp_authority(&self) -> crate::Result<TransactionBuilder<'_, C>> {
313        let builder = AcceptAuthority::builder()
314            .pending_authority(self.payer())
315            .lp_program(self.lp_program_for_builders().clone())
316            .build();
317
318        let ag = builder.into_atomic_group(&())?;
319        Ok(self.store_transaction().pre_atomic_group(ag, true))
320    }
321
322    fn set_claim_enabled(&self, enabled: bool) -> crate::Result<TransactionBuilder<'_, C>> {
323        let builder = SetClaimEnabled::builder()
324            .authority(self.payer())
325            .lp_program(self.lp_program_for_builders().clone())
326            .enabled(enabled)
327            .build();
328
329        let ag = builder.into_atomic_group(&())?;
330        Ok(self.store_transaction().pre_atomic_group(ag, true))
331    }
332
333    fn set_pricing_staleness(
334        &self,
335        staleness_seconds: u32,
336    ) -> crate::Result<TransactionBuilder<'_, C>> {
337        let builder = SetPricingStaleness::builder()
338            .authority(self.payer())
339            .lp_program(self.lp_program_for_builders().clone())
340            .staleness_seconds(staleness_seconds)
341            .build();
342
343        let ag = builder.into_atomic_group(&())?;
344        Ok(self.store_transaction().pre_atomic_group(ag, true))
345    }
346
347    fn update_apy_gradient_sparse(
348        &self,
349        bucket_indices: Vec<u8>,
350        apy_values: Vec<u128>,
351    ) -> crate::Result<TransactionBuilder<'_, C>> {
352        let builder = UpdateApyGradientSparse::builder()
353            .authority(self.payer())
354            .lp_program(self.lp_program_for_builders().clone())
355            .bucket_indices(bucket_indices)
356            .apy_values(apy_values)
357            .build();
358
359        let ag = builder.into_atomic_group(&())?;
360        Ok(self.store_transaction().pre_atomic_group(ag, true))
361    }
362
363    fn update_apy_gradient_range(
364        &self,
365        start_bucket: u8,
366        end_bucket: u8,
367        apy_values: Vec<u128>,
368    ) -> crate::Result<TransactionBuilder<'_, C>> {
369        let builder = UpdateApyGradientRange::builder()
370            .authority(self.payer())
371            .lp_program(self.lp_program_for_builders().clone())
372            .start_bucket(start_bucket)
373            .end_bucket(end_bucket)
374            .apy_values(apy_values)
375            .build();
376
377        let ag = builder.into_atomic_group(&())?;
378        Ok(self.store_transaction().pre_atomic_group(ag, true))
379    }
380
381    fn update_min_stake_value(
382        &self,
383        new_min_stake_value: u128,
384    ) -> crate::Result<TransactionBuilder<'_, C>> {
385        let builder = UpdateMinStakeValue::builder()
386            .authority(self.payer())
387            .lp_program(self.lp_program_for_builders().clone())
388            .new_min_stake_value(new_min_stake_value)
389            .build();
390
391        let ag = builder.into_atomic_group(&())?;
392        Ok(self.store_transaction().pre_atomic_group(ag, true))
393    }
394
395    async fn get_lp_positions(
396        &self,
397        store: &Pubkey,
398        owner: &Pubkey,
399    ) -> crate::Result<Vec<crate::serde::serde_lp_position::SerdeLpStakingPosition>> {
400        let lp_program = self.lp_program_for_builders();
401        lp_program
402            .query_lp_positions(self.rpc(), store, owner)
403            .await
404    }
405
406    async fn get_lp_position(
407        &self,
408        params: LpPositionQueryParams<'_>,
409    ) -> crate::Result<Option<crate::serde::serde_lp_position::SerdeLpStakingPosition>> {
410        let lp_program = self.lp_program_for_builders();
411
412        lp_program.query_lp_position(self.rpc(), &params).await
413    }
414
415    async fn get_my_lp_positions(
416        &self,
417        store: &Pubkey,
418    ) -> crate::Result<Vec<crate::serde::serde_lp_position::SerdeLpStakingPosition>> {
419        self.get_lp_positions(store, &self.payer()).await
420    }
421
422    async fn get_lp_controllers(
423        &self,
424        lp_token_mint: &Pubkey,
425    ) -> crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>> {
426        let lp_program = self.lp_program_for_builders();
427        lp_program
428            .query_lp_controllers(self.rpc(), lp_token_mint)
429            .await
430    }
431
432    async fn get_all_lp_controllers(
433        &self,
434    ) -> crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>> {
435        let lp_program = self.lp_program_for_builders();
436        lp_program.query_all_lp_controllers(self.rpc()).await
437    }
438
439    async fn get_lp_global_state(
440        &self,
441    ) -> crate::Result<crate::serde::serde_lp_global_state::SerdeLpGlobalState> {
442        let lp_program = self.lp_program_for_builders();
443        lp_program.query_lp_global_state(self.rpc()).await
444    }
445}
446
447/// Builder for LP token staking instructions.
448pub struct StakeLpTokenBuilder<'a, C> {
449    client: &'a crate::Client<C>,
450    builder: StakeLpToken,
451    hint: Option<StakeLpTokenHint>,
452}
453
454impl<'a, C: Deref<Target = impl Signer> + Clone> StakeLpTokenBuilder<'a, C> {
455    /// Set a specific position ID instead of using random generation.
456    pub fn with_position_id(mut self, position_id: u64) -> Self {
457        self.builder = self.builder.with_position_id(position_id);
458        self
459    }
460
461    /// Prepare hint.
462    pub async fn prepare_hint(&mut self) -> crate::Result<StakeLpTokenHint> {
463        if let Some(hint) = self.hint.as_ref() {
464            return Ok(hint.clone());
465        }
466        let hint = StakeLpTokenHint::from_rpc_client_with(&self.builder, self.client.rpc()).await?;
467        self.hint = Some(hint.clone());
468        Ok(hint)
469    }
470
471    async fn build_txn(&mut self) -> crate::Result<TransactionBuilder<'a, C>> {
472        let hint = self.prepare_hint().await?;
473        let ag = self.builder.clone().into_atomic_group(&hint)?;
474        let mut txn = self.client.store_transaction().pre_atomic_group(ag, true);
475        txn.compute_budget_mut()
476            .set_limit(STAKE_LP_TOKEN_COMPUTE_BUDGET);
477        Ok(txn)
478    }
479}
480
481impl<'a, C: Deref<Target = impl Signer> + Clone> MakeBundleBuilder<'a, C>
482    for StakeLpTokenBuilder<'a, C>
483{
484    async fn build_with_options(
485        &mut self,
486        options: gmsol_solana_utils::bundle_builder::BundleOptions,
487    ) -> gmsol_solana_utils::Result<gmsol_solana_utils::bundle_builder::BundleBuilder<'a, C>> {
488        let mut tx = self.client.bundle_with_options(options);
489
490        tx.try_push(
491            self.build_txn()
492                .await
493                .map_err(gmsol_solana_utils::Error::custom)?,
494        )?;
495
496        Ok(tx)
497    }
498}
499
500impl<C: Deref<Target = impl Signer> + Clone> PullOraclePriceConsumer
501    for StakeLpTokenBuilder<'_, C>
502{
503    async fn feed_ids(&mut self) -> crate::Result<FeedIds> {
504        let hint = self.prepare_hint().await?;
505        Ok(FeedIds::new(
506            self.builder.store_program.store.0,
507            hint.to_tokens_with_feeds()?,
508        ))
509    }
510
511    fn process_feeds(
512        &mut self,
513        provider: PriceProviderKind,
514        map: FeedAddressMap,
515    ) -> crate::Result<()> {
516        self.builder.insert_feed_parser(provider, map)?;
517        Ok(())
518    }
519}
520
521impl<C> SetExecutionFee for StakeLpTokenBuilder<'_, C> {
522    fn is_execution_fee_estimation_required(&self) -> bool {
523        false
524    }
525
526    fn set_execution_fee(&mut self, _lamports: u64) -> &mut Self {
527        // LP staking doesn't require execution fees, so this is a no-op
528        self
529    }
530}