use std::{collections::BTreeSet, num::NonZeroU64};
use anchor_lang::system_program;
use gmsol_programs::gmsol_liquidity_provider::client::{accounts, args};
use gmsol_solana_utils::{AtomicGroup, IntoAtomicGroup, Program, ProgramExt};
use gmsol_utils::{oracle::PriceProviderKind, token_config::TokensWithFeed};
#[cfg(feature = "client")]
use gmsol_model::utils::apply_factor;
#[cfg(feature = "client")]
use gmsol_programs::{
anchor_lang::Discriminator,
gmsol_store::{
accounts::{Glv, Market, Store},
constants::MARKET_DECIMALS,
},
};
#[cfg(feature = "client")]
use gmsol_solana_utils::client_traits::{FromRpcClientWith, RpcClientExt};
#[cfg(feature = "client")]
use gmsol_utils::{pubkey::optional_address, swap::SwapActionParams, token_config::token_records};
use rand::Rng;
#[cfg(feature = "client")]
use solana_client::{
rpc_config::RpcAccountInfoConfig,
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use typed_builder::TypedBuilder;
#[cfg(feature = "client")]
use crate::client::accounts::{get_program_accounts_with_context, ProgramAccountsConfigForRpc};
use crate::{
serde::{
serde_price_feed::{to_tokens_with_feeds, SerdeTokenRecord},
StringPubkey,
},
utils::{
glv::split_to_accounts,
token_map::{FeedAddressMap, FeedsParser},
},
};
#[cfg(feature = "client")]
use crate::{
serde::serde_lp_position::{
fallback_lp_token_symbol, LpPositionComputedData, SerdeLpStakingPosition,
},
utils::{market::ordered_tokens, token_map::TokenMap, zero_copy::ZeroCopy},
};
use super::StoreProgram;
#[derive(Debug, Clone)]
pub struct LpPositionQueryParams<'a> {
pub store: &'a Pubkey,
pub owner: &'a Pubkey,
pub position_id: u64,
pub lp_token_mint: &'a Pubkey,
pub controller_index: u64,
pub controller_address: Option<&'a Pubkey>,
}
#[derive(Debug, Clone)]
pub struct GtRewardCalculationParams<'a> {
pub store: &'a Pubkey,
pub lp_token_mint: &'a Pubkey,
pub owner: &'a Pubkey,
pub position_id: u64,
pub controller_index: u64,
pub controller_address: Option<&'a Pubkey>,
}
#[derive(Debug, Clone)]
pub struct StakeLpTokenParams<'a> {
pub store: &'a Pubkey,
pub lp_token_kind: LpTokenKind,
pub lp_token_mint: &'a Pubkey,
pub oracle: &'a Pubkey,
pub amount: std::num::NonZeroU64,
pub controller_index: u64,
pub controller_address: Option<Pubkey>,
}
#[derive(Debug, Clone)]
pub struct UnstakeLpTokenParams<'a> {
pub store: &'a Pubkey,
pub lp_token_kind: LpTokenKind,
pub lp_token_mint: &'a Pubkey,
pub position_id: u64,
pub unstake_amount: u64,
pub controller_index: u64,
pub controller_address: Option<Pubkey>,
}
#[cfg(feature = "client")]
fn resolve_controller_address(
lp_program: &LiquidityProviderProgram,
global_state: &Pubkey,
lp_token_mint: &Pubkey,
controller_index: u64,
controller_address: Option<&Pubkey>,
) -> Pubkey {
if let Some(addr) = controller_address {
*addr
} else {
lp_program.find_lp_token_controller_address(global_state, lp_token_mint, controller_index)
}
}
fn resolve_controller_address_for_builder(
lp_program: &LiquidityProviderProgram,
global_state: &Pubkey,
lp_token_mint: &Pubkey,
controller_index: u64,
controller_address: Option<&crate::serde::StringPubkey>,
) -> Pubkey {
if let Some(addr) = controller_address {
addr.0
} else {
lp_program.find_lp_token_controller_address(global_state, lp_token_mint, controller_index)
}
}
const SECONDS_PER_WEEK: u128 = 7 * 24 * 3600;
const APY_LAST_INDEX: usize = 52;
#[cfg(feature = "client")]
const SECONDS_PER_YEAR: u128 = 31_557_600;
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi, into_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct LiquidityProviderProgram {
#[builder(setter(into))]
pub id: StringPubkey,
}
impl Default for LiquidityProviderProgram {
fn default() -> Self {
Self {
id: <Self as anchor_lang::Id>::id().into(),
}
}
}
impl anchor_lang::Id for LiquidityProviderProgram {
fn id() -> Pubkey {
gmsol_programs::gmsol_liquidity_provider::ID
}
}
impl Program for LiquidityProviderProgram {
fn id(&self) -> &Pubkey {
&self.id
}
}
impl LiquidityProviderProgram {
pub fn find_global_state_address(&self) -> Pubkey {
crate::pda::find_lp_global_state_address(&self.id).0
}
#[cfg(feature = "client")]
pub async fn query_lp_positions(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
store: &Pubkey,
owner: &Pubkey,
) -> crate::Result<Vec<crate::serde::serde_lp_position::SerdeLpStakingPosition>> {
let global_state_address = self.find_global_state_address();
let global_state = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState>(
&global_state_address,
Default::default(),
)
.await?;
let store_account = client
.get_anchor_account::<crate::utils::zero_copy::ZeroCopy<gmsol_programs::gmsol_store::accounts::Store>>(store, Default::default())
.await?;
let gt_decimals = store_account.0.gt.decimals;
let config = ProgramAccountsConfigForRpc {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
0,
gmsol_programs::gmsol_liquidity_provider::accounts::Position::DISCRIMINATOR,
)),
RpcFilterType::Memcmp(Memcmp::new(
8,
MemcmpEncodedBytes::Base58(owner.to_string()),
)),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
},
};
let position_accounts_result =
get_program_accounts_with_context(client, &self.id, config).await?;
let position_accounts = position_accounts_result.into_value();
let mut results = Vec::new();
for (_position_address, account) in position_accounts {
let position: gmsol_programs::gmsol_liquidity_provider::accounts::Position =
anchor_lang::AccountDeserialize::try_deserialize(&mut account.data.as_slice())
.map_err(|e| {
crate::Error::custom(format!("Failed to deserialize position: {e}"))
})?;
let controller = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController>(&position.controller, Default::default())
.await?;
let params = GtRewardCalculationParams {
store,
lp_token_mint: &position.lp_mint,
owner: &position.owner,
position_id: position.position_id,
controller_index: controller.controller_index,
controller_address: Some(&position.controller),
};
let actual_gt_reward = self
.calculate_gt_reward_with_store(client, ¶ms, &store_account.0)
.await?;
let serde_position = Self::create_serde_position(
&position,
&controller,
&global_state,
gt_decimals,
actual_gt_reward,
)?;
results.push(serde_position);
}
Ok(results)
}
#[cfg(feature = "client")]
pub async fn query_lp_position(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
params: &LpPositionQueryParams<'_>,
) -> crate::Result<Option<crate::serde::serde_lp_position::SerdeLpStakingPosition>> {
let global_state_address = self.find_global_state_address();
let controller_addr = resolve_controller_address(
self,
&global_state_address,
params.lp_token_mint,
params.controller_index,
params.controller_address,
);
let position_address =
self.find_stake_position_address(params.owner, params.position_id, &controller_addr);
let global_state = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState>(
&global_state_address,
Default::default(),
)
.await?;
let store_account = client
.get_anchor_account::<crate::utils::zero_copy::ZeroCopy<gmsol_programs::gmsol_store::accounts::Store>>(params.store, Default::default())
.await?;
let gt_decimals = store_account.0.gt.decimals;
let position = match client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::Position>(
&position_address,
Default::default(),
)
.await
{
Ok(pos) => pos,
Err(_) => return Ok(None), };
let controller = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController>(&controller_addr, Default::default())
.await?;
let gt_params = GtRewardCalculationParams {
store: params.store,
lp_token_mint: params.lp_token_mint,
owner: params.owner,
position_id: params.position_id,
controller_index: params.controller_index,
controller_address: params.controller_address,
};
let actual_gt_reward = self
.calculate_gt_reward_with_store(client, >_params, &store_account.0)
.await?;
let serde_position = Self::create_serde_position(
&position,
&controller,
&global_state,
gt_decimals,
actual_gt_reward,
)?;
Ok(Some(serde_position))
}
#[cfg(feature = "client")]
pub async fn query_lp_controllers(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
lp_token_mint: &Pubkey,
) -> crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>> {
let all_controllers_config = ProgramAccountsConfigForRpc {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
0,
gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController::DISCRIMINATOR,
)),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
},
};
let all_controller_accounts_result =
get_program_accounts_with_context(client, &self.id, all_controllers_config).await?;
let all_controller_accounts = all_controller_accounts_result.into_value();
let mut results = Vec::new();
for (controller_address, account) in all_controller_accounts {
let controller: gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController =
anchor_lang::AccountDeserialize::try_deserialize(&mut account.data.as_slice())
.map_err(|e| {
crate::Error::custom(format!("Failed to deserialize controller: {e}"))
})?;
if controller.lp_token_mint == *lp_token_mint {
let serde_controller =
crate::serde::serde_lp_controller::SerdeLpController::from_controller(
&controller,
&controller_address,
);
results.push(serde_controller);
}
}
Ok(results)
}
#[cfg(feature = "client")]
pub async fn query_all_lp_controllers(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
) -> crate::Result<Vec<crate::serde::serde_lp_controller::SerdeLpController>> {
let all_controllers_config = ProgramAccountsConfigForRpc {
filters: Some(vec![
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
0,
gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController::DISCRIMINATOR,
)),
]),
account_config: RpcAccountInfoConfig {
encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
},
};
let all_controller_accounts_result =
get_program_accounts_with_context(client, &self.id, all_controllers_config).await?;
let all_controller_accounts = all_controller_accounts_result.into_value();
let mut results = Vec::new();
for (controller_address, account) in all_controller_accounts {
let controller: gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController =
anchor_lang::AccountDeserialize::try_deserialize(&mut account.data.as_slice())
.map_err(|e| {
crate::Error::custom(format!("Failed to deserialize controller: {e}"))
})?;
let serde_controller =
crate::serde::serde_lp_controller::SerdeLpController::from_controller(
&controller,
&controller_address,
);
results.push(serde_controller);
}
Ok(results)
}
#[cfg(feature = "client")]
pub async fn query_lp_global_state(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
) -> crate::Result<crate::serde::serde_lp_global_state::SerdeLpGlobalState> {
let global_state_address = self.find_global_state_address();
let global_state = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState>(
&global_state_address,
Default::default(),
)
.await
.map_err(crate::Error::from)?;
Ok(
crate::serde::serde_lp_global_state::SerdeLpGlobalState::from_global_state(
&global_state,
),
)
}
#[cfg(feature = "client")]
pub async fn calculate_gt_reward(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
params: &GtRewardCalculationParams<'_>,
) -> crate::Result<u128> {
let store_account = client
.get_anchor_account::<crate::utils::zero_copy::ZeroCopy<gmsol_programs::gmsol_store::accounts::Store>>(params.store, Default::default())
.await?;
self.calculate_gt_reward_with_store(client, params, &store_account.0)
.await
}
#[cfg(feature = "client")]
pub async fn calculate_gt_reward_with_store(
&self,
client: &solana_client::nonblocking::rpc_client::RpcClient,
params: &GtRewardCalculationParams<'_>,
store_account: &gmsol_programs::gmsol_store::accounts::Store,
) -> crate::Result<u128> {
let global_state_address = self.find_global_state_address();
let controller_addr = resolve_controller_address(
self,
&global_state_address,
params.lp_token_mint,
params.controller_index,
params.controller_address,
);
let position_address =
self.find_stake_position_address(params.owner, params.position_id, &controller_addr);
let global_state = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState>(
&global_state_address,
Default::default(),
)
.await?;
let position = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::Position>(
&position_address,
Default::default(),
)
.await?;
let controller = client
.get_anchor_account::<gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController>(&controller_addr, Default::default())
.await?;
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let (cum_now, effective_end_time) = if !controller.is_enabled {
(controller.disabled_cum_inv_cost, controller.disabled_at)
} else {
let gt_state = &store_account.gt;
let last_update_time = gt_state.last_cumulative_inv_cost_factor_ts;
let current_cumulative = gt_state.cumulative_inv_cost_factor;
let current_minting_cost = gt_state.minting_cost;
let duration_since_update = current_time.saturating_sub(last_update_time);
let updated_cumulative = if duration_since_update > 0 {
let duration_value = duration_since_update as u128;
let market_usd_unit = 10u128.pow(MARKET_DECIMALS as u32); let delta = if current_minting_cost > 0 {
(duration_value * market_usd_unit) / current_minting_cost
} else {
0 };
current_cumulative.saturating_add(delta)
} else {
current_cumulative
};
(updated_cumulative, current_time)
};
let prev_cum = position.cum_inv_cost;
if cum_now < prev_cum {
return Ok(0); }
let inv_cost_integral = cum_now - prev_cum;
let duration_seconds = effective_end_time.saturating_sub(position.stake_start_time);
if duration_seconds <= 0 {
return Ok(0);
}
let avg_apy = Self::compute_time_weighted_apy(
position.stake_start_time,
effective_end_time,
&global_state.apy_gradient,
);
let avg_apy_per_sec = if SECONDS_PER_YEAR > 0 {
avg_apy / SECONDS_PER_YEAR
} else {
0
};
let per_sec_factor =
apply_factor::<u128, MARKET_DECIMALS>(&position.staked_value_usd, &avg_apy_per_sec)
.ok_or_else(|| {
crate::Error::custom("Math overflow in per_sec_factor calculation")
})?;
let gt_raw = apply_factor::<u128, MARKET_DECIMALS>(&per_sec_factor, &inv_cost_integral)
.ok_or_else(|| crate::Error::custom("Math overflow in gt_raw calculation"))?;
Ok(gt_raw.min(u64::MAX as u128))
}
pub fn compute_current_display_apy(
start_time: i64,
end_time: i64,
apy_gradient: &[u128; 53],
) -> u128 {
if end_time <= start_time {
return apy_gradient[0];
}
let total_seconds: u128 = (end_time - start_time) as u128;
if total_seconds == 0 {
return apy_gradient[0];
}
let week_index = total_seconds / SECONDS_PER_WEEK;
if week_index >= APY_LAST_INDEX as u128 {
apy_gradient[APY_LAST_INDEX] } else {
apy_gradient[week_index as usize]
}
}
pub fn compute_time_weighted_apy(
start_time: i64,
end_time: i64,
apy_gradient: &[u128; 53],
) -> u128 {
if end_time <= start_time {
return apy_gradient[0];
}
let total_seconds: u128 = (end_time - start_time) as u128;
if total_seconds == 0 {
return apy_gradient[0];
}
let full_weeks: u128 = total_seconds / SECONDS_PER_WEEK;
let rem_seconds: u128 = total_seconds % SECONDS_PER_WEEK;
let mut acc: u128 = 0;
let capped_full: u128 = full_weeks.min(APY_LAST_INDEX as u128);
for &apy_value in apy_gradient.iter().take(capped_full as usize) {
acc = acc.saturating_add(apy_value.saturating_mul(SECONDS_PER_WEEK));
}
if full_weeks > (APY_LAST_INDEX as u128) {
let extra = full_weeks - (APY_LAST_INDEX as u128); acc = acc.saturating_add(
apy_gradient[APY_LAST_INDEX].saturating_mul(SECONDS_PER_WEEK.saturating_mul(extra)),
);
}
if rem_seconds > 0 {
let idx =
usize::try_from(full_weeks.min(APY_LAST_INDEX as u128)).unwrap_or(APY_LAST_INDEX);
acc = acc.saturating_add(apy_gradient[idx].saturating_mul(rem_seconds));
}
acc / total_seconds
}
pub fn find_stake_position_address(
&self,
owner: &Pubkey,
position_id: u64,
controller: &Pubkey,
) -> Pubkey {
crate::pda::find_lp_stake_position_address(owner, position_id, controller, &self.id).0
}
pub fn find_stake_position_vault_address(&self, position: &Pubkey) -> Pubkey {
crate::pda::find_lp_stake_position_vault_address(position, &self.id).0
}
pub fn find_lp_token_controller_address(
&self,
global_state: &Pubkey,
lp_token_mint: &Pubkey,
controller_index: u64,
) -> Pubkey {
crate::pda::find_lp_token_controller_address(
global_state,
lp_token_mint,
controller_index,
&self.id,
)
.0
}
#[cfg(feature = "client")]
pub fn create_serde_position(
position: &gmsol_programs::gmsol_liquidity_provider::accounts::Position,
controller: &gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController,
global_state: &gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState,
gt_decimals: u8,
claimable_gt: u128,
) -> crate::Result<crate::serde::serde_lp_position::SerdeLpStakingPosition> {
let computed_data = Self::compute_position_data(position, controller, global_state)?;
let lp_token_symbol = fallback_lp_token_symbol(&position.lp_mint.into());
let computed_data_with_symbol = LpPositionComputedData {
claimable_gt: crate::utils::Amount::from_u128(claimable_gt, gt_decimals).map_err(
|_| crate::Error::custom("Claimable GT amount exceeds maximum representable value"),
)?,
current_apy: crate::utils::Value::from_u128(computed_data.current_apy),
average_apy: crate::utils::Value::from_u128(computed_data.average_apy),
lp_token_symbol,
};
SerdeLpStakingPosition::from_position(position, controller, computed_data_with_symbol)
}
#[cfg(feature = "client")]
fn compute_position_data(
position: &gmsol_programs::gmsol_liquidity_provider::accounts::Position,
controller: &gmsol_programs::gmsol_liquidity_provider::accounts::LpTokenController,
global_state: &gmsol_programs::gmsol_liquidity_provider::accounts::GlobalState,
) -> crate::Result<PositionComputedData> {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let effective_end_time = if controller.is_enabled {
current_time
} else {
controller.disabled_at
};
let current_display_apy = Self::compute_current_display_apy(
position.stake_start_time,
effective_end_time,
&global_state.apy_gradient,
);
let average_apy = Self::compute_time_weighted_apy(
position.stake_start_time,
effective_end_time,
&global_state.apy_gradient,
);
Ok(PositionComputedData {
current_apy: current_display_apy, average_apy, })
}
}
#[cfg(feature = "client")]
struct PositionComputedData {
current_apy: u128,
average_apy: u128,
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct StakeLpToken {
#[builder(setter(into))]
pub payer: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub store_program: StoreProgram,
#[builder(setter(into))]
pub oracle: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub lp_token_kind: LpTokenKind,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub lp_token_account: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub position_id: Option<u64>,
pub amount: NonZeroU64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub controller_index: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub controller_address: Option<StringPubkey>,
#[cfg_attr(serde, serde(skip))]
#[builder(default)]
pub feeds_parser: FeedsParser,
}
impl StakeLpToken {
pub fn insert_feed_parser(
&mut self,
provider: PriceProviderKind,
map: FeedAddressMap,
) -> crate::Result<()> {
self.feeds_parser
.insert_pull_oracle_feed_parser(provider, map);
Ok(())
}
pub fn with_position_id(mut self, position_id: u64) -> Self {
self.position_id = Some(position_id);
self
}
fn position_id(&self) -> u64 {
self.position_id.unwrap_or_else(|| rand::thread_rng().gen())
}
fn lp_token_account(&self, token_program_id: &Pubkey) -> Pubkey {
self.lp_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
anchor_spl::associated_token::get_associated_token_address_with_program_id(
&self.payer,
&self.lp_token_mint,
token_program_id,
)
})
}
fn shared_args(&self) -> SharedArgs {
let owner = self.payer.0;
let position_id = self.position_id();
let global_state = self.lp_program.find_global_state_address();
let lp_mint = self.lp_token_mint.0;
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
let position =
self.lp_program
.find_stake_position_address(&owner, position_id, &controller);
let position_vault = self.lp_program.find_stake_position_vault_address(&position);
SharedArgs {
owner,
position_id,
global_state,
lp_mint,
position,
position_vault,
gt_store: self.store_program.store.0,
gt_program: *self.store_program.id(),
}
}
fn feeds(&self, hint: &StakeLpTokenHint) -> gmsol_solana_utils::Result<Vec<AccountMeta>> {
self.feeds_parser
.parse(&hint.to_tokens_with_feeds()?)
.collect::<Result<Vec<_>, _>>()
.map_err(gmsol_solana_utils::Error::custom)
}
fn stake_gm(&self, hint: &StakeLpTokenHint) -> gmsol_solana_utils::Result<Instruction> {
let SharedArgs {
owner,
position_id,
global_state,
lp_mint,
position,
position_vault,
gt_store,
gt_program,
} = self.shared_args();
let token_program_id = anchor_spl::token::ID;
let market = self.store_program.find_market_address(&lp_mint);
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
Ok(self
.lp_program
.anchor_instruction(args::StakeGm {
position_id,
gm_staked_amount: self.amount.get(),
})
.anchor_accounts(
accounts::StakeGm {
global_state,
controller,
lp_mint,
position,
position_vault,
gt_store,
gt_program,
owner,
user_lp_token: self.lp_token_account(&token_program_id),
token_map: hint.token_map.0,
oracle: self.oracle.0,
market,
system_program: system_program::ID,
token_program: token_program_id,
event_authority: self.store_program.find_event_authority_address(),
},
false,
)
.accounts(self.feeds(hint)?)
.build())
}
fn stake_glv(&self, hint: &StakeLpTokenHint) -> gmsol_solana_utils::Result<Instruction> {
let SharedArgs {
owner,
position_id,
global_state,
lp_mint,
position,
position_vault,
gt_store,
gt_program,
} = self.shared_args();
let token_program_id = anchor_spl::token_2022::ID;
let glv = self.store_program.find_glv_address(&lp_mint);
let market_tokens = hint.glv_market_tokens.as_ref().ok_or_else(|| {
gmsol_solana_utils::Error::custom("Hint must include the market token list for the GLV")
})?;
let glv_accounts = split_to_accounts(
market_tokens.iter().map(|token| token.0),
&glv,
>_store,
>_program,
&token_program_id,
false,
)
.0;
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
Ok(self
.lp_program
.anchor_instruction(args::StakeGlv {
position_id,
glv_staked_amount: self.amount.get(),
})
.anchor_accounts(
accounts::StakeGlv {
global_state,
controller,
lp_mint,
position,
position_vault,
gt_store,
gt_program,
owner,
user_lp_token: self.lp_token_account(&token_program_id),
token_map: hint.token_map.0,
oracle: self.oracle.0,
glv,
system_program: system_program::ID,
token_program: token_program_id,
event_authority: self.store_program.find_event_authority_address(),
},
false,
)
.accounts(glv_accounts)
.accounts(self.feeds(hint)?)
.build())
}
}
impl IntoAtomicGroup for StakeLpToken {
type Hint = StakeLpTokenHint;
fn into_atomic_group(self, hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let owner = self.payer.0;
let mut insts = AtomicGroup::new(&owner);
let stake = match self.lp_token_kind {
LpTokenKind::Gm => self.stake_gm(hint),
LpTokenKind::Glv => self.stake_glv(hint),
}?;
insts.add(stake);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct StakeLpTokenHint {
#[builder(setter(into))]
pub token_map: StringPubkey,
#[builder(setter(into))]
pub feeds: Vec<SerdeTokenRecord>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub glv_market_tokens: Option<BTreeSet<StringPubkey>>,
}
impl StakeLpTokenHint {
pub fn to_tokens_with_feeds(&self) -> gmsol_solana_utils::Result<TokensWithFeed> {
to_tokens_with_feeds(&self.feeds).map_err(gmsol_solana_utils::Error::custom)
}
}
#[cfg(feature = "client")]
impl FromRpcClientWith<StakeLpToken> for StakeLpTokenHint {
async fn from_rpc_client_with<'a>(
builder: &'a StakeLpToken,
client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
) -> gmsol_solana_utils::Result<Self> {
let store_program = &builder.store_program;
let store_address = &store_program.store.0;
let store = client
.get_anchor_account::<ZeroCopy<Store>>(store_address, Default::default())
.await?
.0;
let token_map_address = optional_address(&store.token_map)
.ok_or_else(|| gmsol_solana_utils::Error::custom("token map is not set"))?;
let (tokens, glv_market_tokens) = match builder.lp_token_kind {
LpTokenKind::Gm => {
let market_address = store_program.find_market_address(&builder.lp_token_mint);
let market = client
.get_anchor_account::<ZeroCopy<Market>>(&market_address, Default::default())
.await?
.0;
(ordered_tokens(&market.meta.into()), None)
}
LpTokenKind::Glv => {
let glv_address = store_program.find_glv_address(&builder.lp_token_mint);
let glv = client
.get_anchor_account::<ZeroCopy<Glv>>(&glv_address, Default::default())
.await?
.0;
let mut collector = glv.tokens_collector(None::<&SwapActionParams>);
for token in glv.market_tokens() {
let market_address = store_program.find_market_address(&token);
let market = client
.get_anchor_account::<ZeroCopy<Market>>(&market_address, Default::default())
.await?
.0;
collector.insert_token(&market.meta.index_token_mint);
}
let market_tokens = glv.market_tokens().map(StringPubkey).collect();
(collector.unique_tokens(), Some(market_tokens))
}
};
let token_map = client
.get_anchor_account::<TokenMap>(token_map_address, Default::default())
.await?;
let feeds = token_records(&token_map, &tokens)
.map_err(gmsol_solana_utils::Error::custom)?
.into_iter()
.map(SerdeTokenRecord::try_from)
.collect::<Result<Vec<_>, _>>()
.map_err(gmsol_solana_utils::Error::custom)?;
Ok(Self {
token_map: (*token_map_address).into(),
feeds,
glv_market_tokens,
})
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(Debug, Clone, Copy)]
pub enum LpTokenKind {
Gm,
Glv,
}
struct SharedArgs {
owner: Pubkey,
position_id: u64,
global_state: Pubkey,
lp_mint: Pubkey,
position: Pubkey,
position_vault: Pubkey,
gt_store: Pubkey,
gt_program: Pubkey,
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct InitializeLp {
#[builder(setter(into))]
pub payer: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub min_stake_value: u128,
pub initial_apy: u128,
}
impl IntoAtomicGroup for InitializeLp {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let payer = self.payer.0;
let mut insts = AtomicGroup::new(&payer);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::Initialize {
min_stake_value: self.min_stake_value,
initial_apy: self.initial_apy,
})
.anchor_accounts(
accounts::Initialize {
global_state,
authority: payer,
system_program: system_program::ID,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct CreateLpTokenController {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
pub controller_index: u64,
}
impl IntoAtomicGroup for CreateLpTokenController {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let controller = self.lp_program.find_lp_token_controller_address(
&global_state,
&self.lp_token_mint.0,
self.controller_index,
);
let instruction = self
.lp_program
.anchor_instruction(args::CreateLpTokenController {
lp_token_mint: self.lp_token_mint.0,
controller_index: self.controller_index,
})
.anchor_accounts(
accounts::CreateLpTokenController {
global_state,
controller,
authority,
system_program: system_program::ID,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct DisableLpTokenController {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub store_program: StoreProgram,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub controller_index: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub controller_address: Option<StringPubkey>,
}
impl IntoAtomicGroup for DisableLpTokenController {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&self.lp_token_mint.0,
self.controller_index,
self.controller_address.as_ref(),
);
let instruction = self
.lp_program
.anchor_instruction(args::DisableLpTokenController {})
.anchor_accounts(
accounts::DisableLpTokenController {
global_state,
controller,
gt_store: self.store_program.store.0,
gt_program: *self.store_program.id(),
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct UnstakeLpToken {
#[builder(setter(into))]
pub payer: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub store_program: StoreProgram,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub lp_token_kind: LpTokenKind,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub lp_token_account: Option<StringPubkey>,
pub position_id: u64,
pub unstake_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub controller_index: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub controller_address: Option<StringPubkey>,
}
impl UnstakeLpToken {
fn lp_token_account(&self, token_program_id: &Pubkey) -> Pubkey {
self.lp_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
anchor_spl::associated_token::get_associated_token_address_with_program_id(
&self.payer,
&self.lp_token_mint,
token_program_id,
)
})
}
fn shared_unstake_args(&self) -> SharedUnstakeArgs {
let owner = self.payer.0;
let global_state = self.lp_program.find_global_state_address();
let lp_mint = self.lp_token_mint.0;
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
let position =
self.lp_program
.find_stake_position_address(&owner, self.position_id, &controller);
let position_vault = self.lp_program.find_stake_position_vault_address(&position);
SharedUnstakeArgs {
owner,
global_state,
lp_mint,
controller,
position,
position_vault,
gt_store: self.store_program.store.0,
gt_program: *self.store_program.id(),
}
}
}
impl IntoAtomicGroup for UnstakeLpToken {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let owner = self.payer.0;
let mut insts = AtomicGroup::new(&owner);
let SharedUnstakeArgs {
owner,
global_state,
lp_mint,
controller,
position,
position_vault,
gt_store,
gt_program,
} = self.shared_unstake_args();
let gt_user = crate::pda::find_user_address(>_store, &owner, >_program).0;
let event_authority = self.store_program.find_event_authority_address();
let token_program_id = match self.lp_token_kind {
LpTokenKind::Gm => anchor_spl::token::ID,
LpTokenKind::Glv => anchor_spl::token_2022::ID,
};
let instruction = self
.lp_program
.anchor_instruction(args::UnstakeLp {
_position_id: self.position_id,
unstake_amount: self.unstake_amount,
})
.anchor_accounts(
accounts::UnstakeLp {
global_state,
controller,
lp_mint,
store: gt_store,
gt_program,
position,
position_vault,
owner,
gt_user,
user_lp_token: self.lp_token_account(&token_program_id),
event_authority,
token_program: token_program_id,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
struct SharedUnstakeArgs {
owner: Pubkey,
global_state: Pubkey,
lp_mint: Pubkey,
controller: Pubkey,
position: Pubkey,
position_vault: Pubkey,
gt_store: Pubkey,
gt_program: Pubkey,
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct TransferAuthority {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
#[builder(setter(into))]
pub new_authority: StringPubkey,
}
impl IntoAtomicGroup for TransferAuthority {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::TransferAuthority {
new_authority: self.new_authority.0,
})
.anchor_accounts(
accounts::TransferAuthority {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct AcceptAuthority {
#[builder(setter(into))]
pub pending_authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
}
impl IntoAtomicGroup for AcceptAuthority {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let pending_authority = self.pending_authority.0;
let mut insts = AtomicGroup::new(&pending_authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::AcceptAuthority {})
.anchor_accounts(
accounts::AcceptAuthority {
global_state,
pending_authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct SetPricingStaleness {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub staleness_seconds: u32,
}
impl IntoAtomicGroup for SetPricingStaleness {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::SetPricingStaleness {
staleness_seconds: self.staleness_seconds,
})
.anchor_accounts(
accounts::SetPricingStaleness {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct SetClaimEnabled {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub enabled: bool,
}
impl IntoAtomicGroup for SetClaimEnabled {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::SetClaimEnabled {
enabled: self.enabled,
})
.anchor_accounts(
accounts::SetClaimEnabled {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct UpdateApyGradientSparse {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub bucket_indices: Vec<u8>,
pub apy_values: Vec<u128>,
}
impl IntoAtomicGroup for UpdateApyGradientSparse {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::UpdateApyGradientSparse {
bucket_indices: self.bucket_indices,
apy_values: self.apy_values,
})
.anchor_accounts(
accounts::UpdateApyGradientSparse {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct UpdateApyGradientRange {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub start_bucket: u8,
pub end_bucket: u8,
pub apy_values: Vec<u128>,
}
impl IntoAtomicGroup for UpdateApyGradientRange {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::UpdateApyGradientRange {
start_bucket: self.start_bucket,
end_bucket: self.end_bucket,
apy_values: self.apy_values,
})
.anchor_accounts(
accounts::UpdateApyGradientRange {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct UpdateMinStakeValue {
#[builder(setter(into))]
pub authority: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
pub new_min_stake_value: u128,
}
impl IntoAtomicGroup for UpdateMinStakeValue {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let authority = self.authority.0;
let mut insts = AtomicGroup::new(&authority);
let global_state = self.lp_program.find_global_state_address();
let instruction = self
.lp_program
.anchor_instruction(args::UpdateMinStakeValue {
new_min_stake_value: self.new_min_stake_value,
})
.anchor_accounts(
accounts::UpdateMinStakeValue {
global_state,
authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct CalculateGtReward {
#[builder(setter(into))]
pub owner: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub store_program: StoreProgram,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
pub position_id: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub controller_index: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub controller_address: Option<StringPubkey>,
}
#[cfg_attr(js, derive(tsify_next::Tsify))]
#[cfg_attr(js, tsify(from_wasm_abi))]
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, TypedBuilder)]
pub struct ClaimGtReward {
#[builder(setter(into))]
pub owner: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub store_program: StoreProgram,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub lp_program: LiquidityProviderProgram,
#[builder(setter(into))]
pub lp_token_mint: StringPubkey,
pub position_id: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub controller_index: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub controller_address: Option<StringPubkey>,
}
impl IntoAtomicGroup for CalculateGtReward {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let owner = self.owner.0;
let mut insts = AtomicGroup::new(&owner);
let global_state = self.lp_program.find_global_state_address();
let lp_mint = self.lp_token_mint.0;
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
let position =
self.lp_program
.find_stake_position_address(&owner, self.position_id, &controller);
let instruction = self
.lp_program
.anchor_instruction(args::CalculateGtReward {})
.anchor_accounts(
accounts::CalculateGtReward {
global_state,
controller,
gt_store: self.store_program.store.0,
gt_program: *self.store_program.id(),
position,
owner,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}
impl IntoAtomicGroup for ClaimGtReward {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
let owner = self.owner.0;
let mut insts = AtomicGroup::new(&owner);
let global_state = self.lp_program.find_global_state_address();
let lp_mint = self.lp_token_mint.0;
let controller = resolve_controller_address_for_builder(
&self.lp_program,
&global_state,
&lp_mint,
self.controller_index,
self.controller_address.as_ref(),
);
let position =
self.lp_program
.find_stake_position_address(&owner, self.position_id, &controller);
let gt_user = crate::pda::find_user_address(
&self.store_program.store.0,
&owner,
self.store_program.id(),
)
.0;
let event_authority = self.store_program.find_event_authority_address();
let instruction = self
.lp_program
.anchor_instruction(args::ClaimGt {
_position_id: self.position_id,
})
.anchor_accounts(
accounts::ClaimGt {
global_state,
controller,
store: self.store_program.store.0,
gt_program: *self.store_program.id(),
position,
owner,
gt_user,
event_authority,
},
false,
)
.build();
insts.add(instruction);
Ok(insts)
}
}