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::CreateShiftParams,
};
use gmsol_solana_utils::{AtomicGroup, IntoAtomicGroup, ProgramExt};
use solana_sdk::system_program;
use typed_builder::TypedBuilder;
use crate::{
builders::{
shift::MIN_EXECUTION_LAMPORTS_FOR_SHIFT,
utils::{generate_nonce, prepare_ata},
MarketTokenIxBuilder, NonceBytes, 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 CreateShift {
#[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 from_market_token: StringPubkey,
#[cfg_attr(serde, serde(default))]
#[builder(default, setter(into))]
pub from_market_token_account: Option<StringPubkey>,
#[builder(setter(into))]
pub to_market_token: StringPubkey,
#[cfg_attr(serde, serde(default = "default_execution_lamports"))]
#[builder(default = MIN_EXECUTION_LAMPORTS_FOR_SHIFT)]
pub execution_lamports: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub from_market_token_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub min_to_market_token_amount: u64,
#[cfg_attr(serde, serde(default))]
#[builder(default)]
pub skip_to_market_token_ata_creation: bool,
}
#[cfg(serde)]
fn default_execution_lamports() -> u64 {
MIN_EXECUTION_LAMPORTS_FOR_SHIFT
}
impl StoreProgramIxBuilder for CreateShift {
fn store_program(&self) -> &StoreProgram {
&self.program
}
}
impl MarketTokenIxBuilder for CreateShift {
fn market_token(&self) -> &anchor_lang::prelude::Pubkey {
&self.from_market_token
}
}
impl IntoAtomicGroup for CreateShift {
type Hint = ();
fn into_atomic_group(self, _hint: &Self::Hint) -> gmsol_solana_utils::Result<AtomicGroup> {
if self.from_market_token_amount.is_zero() {
return Err(gmsol_solana_utils::Error::custom(
"invalid argument: empty shift",
));
}
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 shift = self.program.find_shift_address(&owner, &nonce);
let token_program_id = anchor_spl::token::ID;
let from_market_token = self.from_market_token.0;
let to_market_token = self.to_market_token.0;
let from_market_token_account = self
.from_market_token_account
.as_deref()
.copied()
.unwrap_or_else(|| {
get_associated_token_address_with_program_id(
&owner,
&from_market_token,
&token_program_id,
)
});
let (from_market_token_escrow, prepare) =
prepare_ata(&owner, &shift, Some(&from_market_token), &token_program_id)
.expect("must exist");
insts.add(prepare);
let (to_market_token_escrow, prepare) =
prepare_ata(&owner, &shift, Some(&to_market_token), &token_program_id)
.expect("must exist");
insts.add(prepare);
let (to_market_token_ata, prepare) =
prepare_ata(&owner, &receiver, Some(&to_market_token), &token_program_id)
.expect("must exist");
if !self.skip_to_market_token_ata_creation {
insts.add(prepare);
}
let params = CreateShiftParams {
execution_lamports: self.execution_lamports,
from_market_token_amount: self.from_market_token_amount,
min_to_market_token_amount: self.min_to_market_token_amount,
};
let create = self
.program
.anchor_instruction(args::CreateShift {
nonce: nonce.to_bytes(),
params,
})
.anchor_accounts(
accounts::CreateShift {
owner,
receiver,
store: self.program.store.0,
from_market: self.program.find_market_address(&from_market_token),
to_market: self.program.find_market_address(&to_market_token),
shift,
from_market_token,
to_market_token,
from_market_token_escrow,
to_market_token_escrow,
from_market_token_source: from_market_token_account,
to_market_token_ata,
system_program: system_program::ID,
token_program: token_program_id,
associated_token_program: associated_token::ID,
},
false,
)
.build();
insts.add(create);
Ok(insts)
}
}
#[cfg(test)]
mod tests {
use gmsol_solana_utils::transaction_builder::default_before_sign;
use solana_sdk::pubkey::Pubkey;
use super::*;
#[test]
fn create_shift() -> crate::Result<()> {
CreateShift::builder()
.payer(Pubkey::new_unique())
.from_market_token(Pubkey::new_unique())
.to_market_token(Pubkey::new_unique())
.from_market_token_amount(1_000_000_000)
.build()
.into_atomic_group(&())?
.partially_signed_transaction_with_blockhash_and_options(
Default::default(),
Default::default(),
None,
default_before_sign,
)?;
Ok(())
}
}