use anchor_spl::associated_token::{self, get_associated_token_address_with_program_id};
use gmsol_model::num_traits::Zero;
use gmsol_programs::gmsol_store::{
client::{accounts, args},
types::CreateGlvDepositParams,
};
use gmsol_solana_utils::{
client_traits::FromRpcClientWith, AtomicGroup, IntoAtomicGroup, ProgramExt,
};
use solana_sdk::{instruction::AccountMeta, system_program};
use typed_builder::TypedBuilder;
use crate::{
builders::{
glv_deposit::MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT,
utils::{generate_nonce, prepare_ata},
MarketTokenIxBuilder, NonceBytes, PoolTokenHint, StoreProgram, StoreProgramIxBuilder,
},
serde::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 CreateGlvDeposit {
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub program: StoreProgram,
#[builder(setter(into))]
pub payer: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(strip_option, into))]
pub receiver: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(strip_option, into))]
pub nonce: Option<NonceBytes>,
#[builder(setter(into))]
pub glv_token: StringPubkey,
#[builder(setter(into))]
pub market_token: StringPubkey,
#[cfg_attr(serde, serde(default = "default_execution_lamports"))]
#[builder(default = MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT)]
pub execution_lamports: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub long_pay_token: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub long_pay_token_account: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub long_swap_path: Vec<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub short_pay_token: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub short_pay_token_account: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub short_swap_path: Vec<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub market_token_account: Option<StringPubkey>,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub long_pay_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub short_pay_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub market_token_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub min_market_token_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub min_receive_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub unwrap_native_on_receive: bool,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub skip_glv_token_ata_creation: bool,
}
#[cfg(serde)]
fn default_execution_lamports() -> u64 {
MIN_EXECUTION_LAMPORTS_FOR_GLV_DEPOSIT
}
impl StoreProgramIxBuilder for CreateGlvDeposit {
fn store_program(&self) -> &StoreProgram {
&self.program
}
}
impl MarketTokenIxBuilder for CreateGlvDeposit {
fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
&self.market_token
}
}
impl IntoAtomicGroup for CreateGlvDeposit {
type Hint = CreateGlvDepositHint;
fn into_atomic_group(self, hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
if self.long_pay_amount.is_zero()
&& self.short_pay_amount.is_zero()
&& self.market_token_amount.is_zero()
{
return Err(gmsol_solana_utils::Error::custom(
"invalid argument: empty GLV deposit",
));
}
let owner = self.payer.0;
let mut insts = AtomicGroup::new(&owner);
let receiver = self.receiver.as_deref().copied().unwrap_or(owner);
let nonce = self.nonce.unwrap_or_else(generate_nonce);
let glv_deposit = self.program.find_glv_deposit_address(&owner, &nonce);
let token_program_id = anchor_spl::token::ID;
let glv_token_program_id = anchor_spl::token_2022::ID;
let market_token = self.market_token.0;
let glv_token = self.glv_token.0;
let long_pay_token = (!self.long_pay_amount.is_zero()).then(|| {
self.long_pay_token
.as_deref()
.unwrap_or(&hint.pool_tokens.long_token)
});
let long_pay_token_account = long_pay_token.as_ref().map(|token| {
self.long_pay_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
get_associated_token_address_with_program_id(&owner, token, &token_program_id)
})
});
let short_pay_token = (!self.short_pay_amount.is_zero()).then(|| {
self.short_pay_token
.as_deref()
.unwrap_or(&hint.pool_tokens.short_token)
});
let short_pay_token_account = short_pay_token.as_ref().map(|token| {
self.short_pay_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
get_associated_token_address_with_program_id(&owner, token, &token_program_id)
})
});
let market_token_account = (!self.market_token_amount.is_zero()).then(|| {
self.market_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
get_associated_token_address_with_program_id(
&owner,
&market_token,
&token_program_id,
)
})
});
let (long_pay_token_escrow, prepare) =
prepare_ata(&owner, &glv_deposit, long_pay_token, &token_program_id).unzip();
insts.extend(prepare);
let (short_pay_token_escrow, prepare) =
prepare_ata(&owner, &glv_deposit, short_pay_token, &token_program_id).unzip();
insts.extend(prepare);
let (market_token_escrow, prepare) =
prepare_ata(&owner, &glv_deposit, Some(&market_token), &token_program_id)
.expect("must exist");
insts.add(prepare);
let (glv_token_escrow, prepare) = prepare_ata(
&owner,
&glv_deposit,
Some(&glv_token),
&glv_token_program_id,
)
.expect("must exist");
insts.add(prepare);
let (_glv_token_ata, prepare) =
prepare_ata(&owner, &receiver, Some(&glv_token), &glv_token_program_id)
.expect("must exist");
if !self.skip_glv_token_ata_creation {
insts.add(prepare);
}
let params = CreateGlvDepositParams {
execution_lamports: self.execution_lamports,
long_token_swap_length: self
.long_swap_path
.len()
.try_into()
.map_err(gmsol_solana_utils::Error::custom)?,
short_token_swap_length: self
.short_swap_path
.len()
.try_into()
.map_err(gmsol_solana_utils::Error::custom)?,
initial_long_token_amount: self.long_pay_amount,
initial_short_token_amount: self.short_pay_amount,
market_token_amount: self.market_token_amount,
min_market_token_amount: self.min_market_token_amount,
min_glv_token_amount: self.min_receive_amount,
should_unwrap_native_token: self.unwrap_native_on_receive,
};
let create = self
.program
.anchor_instruction(args::CreateGlvDeposit {
nonce: nonce.to_bytes(),
params,
})
.anchor_accounts(
accounts::CreateGlvDeposit {
owner,
receiver,
store: self.program.store.0,
glv: self.program.find_glv_address(&glv_token),
market: self.program.find_market_address(&market_token),
glv_deposit,
glv_token,
market_token,
initial_long_token: long_pay_token.copied(),
initial_short_token: short_pay_token.copied(),
glv_token_escrow,
market_token_escrow,
initial_long_token_escrow: long_pay_token_escrow,
initial_short_token_escrow: short_pay_token_escrow,
initial_long_token_source: long_pay_token_account,
initial_short_token_source: short_pay_token_account,
market_token_source: market_token_account,
system_program: system_program::ID,
token_program: token_program_id,
glv_token_program: glv_token_program_id,
associated_token_program: associated_token::ID,
},
true,
)
.accounts(
self.long_swap_path
.iter()
.chain(self.short_swap_path.iter())
.map(|token| AccountMeta {
pubkey: self.program.find_market_address(token),
is_signer: false,
is_writable: false,
})
.collect::<Vec<_>>(),
)
.build();
insts.add(create);
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 CreateGlvDepositHint {
#[builder(setter(into))]
pub pool_tokens: PoolTokenHint,
}
impl FromRpcClientWith<CreateGlvDeposit> for CreateGlvDepositHint {
async fn from_rpc_client_with<'a>(
builder: &'a CreateGlvDeposit,
client: &'a impl gmsol_solana_utils::client_traits::RpcClient,
) -> gmsol_solana_utils::Result<Self> {
let pool_tokens = PoolTokenHint::from_rpc_client_with(builder, client).await?;
Ok(Self { pool_tokens })
}
}
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
use tokio::test as async_test;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as async_test;
use gmsol_solana_utils::{
client_traits::GenericRpcClient, cluster::Cluster, transaction_builder::default_before_sign,
};
use solana_sdk::pubkey::Pubkey;
use super::*;
#[test]
fn create_glv_deposit() -> crate::Result<()> {
let long_token = Pubkey::new_unique();
let short_token = Pubkey::new_unique();
CreateGlvDeposit::builder()
.payer(Pubkey::new_unique())
.long_swap_path([Pubkey::new_unique().into()])
.long_pay_amount(1_000_000_000)
.long_pay_token(Some(Pubkey::new_unique().into()))
.glv_token(Pubkey::new_unique())
.market_token(Pubkey::new_unique())
.unwrap_native_on_receive(true)
.build()
.into_atomic_group(
&CreateGlvDepositHint::builder()
.pool_tokens(
PoolTokenHint::builder()
.long_token(long_token)
.short_token(short_token)
.build(),
)
.build(),
)?
.partially_signed_transaction_with_blockhash_and_options(
Default::default(),
Default::default(),
None,
default_before_sign,
)?;
Ok(())
}
#[async_test]
async fn create_glv_deposit_with_rpc() -> crate::Result<()> {
let market_token: Pubkey = "5sdFW7wrKsxxYHMXoqDmNHkGyCWsbLEFb1x1gzBBm4Hx".parse()?;
let wsol: Pubkey = "So11111111111111111111111111111111111111112".parse()?;
let cluster = Cluster::Devnet;
let client = GenericRpcClient::new(cluster.url());
CreateGlvDeposit::builder()
.payer(Pubkey::new_unique())
.short_swap_path([Pubkey::new_unique().into()])
.short_pay_amount(1_000_000_000)
.short_pay_token(Some(wsol.into()))
.glv_token(Pubkey::new_unique())
.market_token(market_token)
.unwrap_native_on_receive(true)
.build()
.into_atomic_group_with_rpc_client(&client)
.await?
.partially_signed_transaction_with_blockhash_and_options(
Default::default(),
Default::default(),
None,
default_before_sign,
)?;
Ok(())
}
}