use solana_address::Address;
use solana_instruction::Instruction;
use spl_tlv_account_resolution::solana_program_error::ProgramError;
use super::BuildError;
use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS};
use crate::math::amm::MintFee;
use crate::transfer_fee::FeeSchedule;
use crate::transfer_hook::{
AccountView, Fetched, FetchedAccount, HookError, HookLegs, HookState,
MintState, Step, find_validation_address, resolve_market,
};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MintInfo {
pub address: Address,
pub token_program: Address,
pub decimals: u8,
pub transfer_fee: Option<FeeSchedule>,
pub hook_program: Option<Address>,
pub hook: Option<HookState>,
}
impl MintInfo {
pub fn load(
address: Address,
view: AccountView<'_>,
clock: AccountView<'_>,
) -> Result<Self, ProgramError> {
Self::load_at(address, view, clock_epoch(clock)?)
}
pub fn load_at(
address: Address,
view: AccountView<'_>,
epoch: u64,
) -> Result<Self, ProgramError> {
let state = MintState::load(view)?;
let hook = state.transfer_hook.map(|_| {
let mut hook = HookState::default();
hook.insert(
address,
Some(FetchedAccount {
owner: *view.owner,
data: view.data.to_vec(),
}),
);
hook
});
Ok(Self {
address,
token_program: state.token_program,
decimals: state.decimals,
transfer_fee: state
.transfer_fee
.map(|config| FeeSchedule::of(&config, epoch)),
hook_program: state.transfer_hook,
hook,
})
}
#[must_use]
pub const fn plain(
address: Address,
token_program: Address,
decimals: u8,
) -> Self {
Self {
address,
token_program,
decimals,
transfer_fee: None,
hook_program: None,
hook: None,
}
}
#[must_use]
pub const fn launch(address: Address) -> Self {
Self::plain(address, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
}
#[must_use]
pub fn validation_address(&self) -> Option<Address> {
self.hook_program
.map(|program| find_validation_address(&self.address, &program))
}
pub(crate) const fn hook_not_read(&self) -> bool {
self.hook_program.is_some() && self.hook.is_none()
}
pub fn missing(&self) -> Result<Vec<Address>, HookError> {
self.hook.as_ref().map_or_else(
|| Ok(Vec::new()),
|hook| hook.missing(&[self.address]),
)
}
pub(crate) fn fetched(&self, key: &Address) -> Fetched<'_> {
self.hook
.as_ref()
.map_or(Fetched::NotFetched, |hook| hook.fetched(key))
}
pub fn insert(
&mut self,
address: Address,
account: Option<FetchedAccount>,
) {
if let Some(hook) = &mut self.hook {
hook.insert(address, account);
}
}
}
const SYSVAR_PROGRAM_ID: Address =
solana_address::address!("Sysvar1111111111111111111111111111111111111");
const CLOCK_LEN: usize = 40;
const CLOCK_EPOCH: core::ops::Range<usize> = 16..24;
fn clock_epoch(clock: AccountView<'_>) -> Result<u64, ProgramError> {
if *clock.owner != SYSVAR_PROGRAM_ID {
return Err(ProgramError::IncorrectProgramId);
}
if clock.data.len() != CLOCK_LEN {
return Err(ProgramError::InvalidAccountData);
}
clock
.data
.get(CLOCK_EPOCH)
.and_then(|bytes| bytes.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidAccountData)
}
#[derive(Clone, Copy)]
pub struct TradeMints<'a> {
pub base: &'a MintInfo,
pub quote: &'a MintInfo,
}
pub struct TradeFees {
pub base: Option<MintFee>,
pub quote: Option<MintFee>,
}
pub fn trade_fees(mints: TradeMints<'_>) -> Result<TradeFees, BuildError> {
check_base_mint(mints.base)?;
Ok(TradeFees {
base: mints.base.transfer_fee.map(|fee| fee.fee()),
quote: mints.quote.transfer_fee.map(|fee| fee.fee()),
})
}
pub fn check_base_mint(base: &MintInfo) -> Result<(), BuildError> {
if base.token_program != TOKEN_2022_PROGRAM_ID {
return Err(BuildError::BaseNotToken2022(base.address));
}
Ok(())
}
pub fn append_hooks(
instruction: Instruction,
mints: TradeMints<'_>,
legs: &HookLegs,
) -> Result<Instruction, BuildError> {
if let Some(info) = [mints.base, mints.quote]
.into_iter()
.find(|info| info.hook_not_read())
{
return Err(BuildError::HookNotRead(info.address));
}
match resolve_market(mints, legs, |_| Fetched::NotFetched)? {
Step::Done(resolved) => {
let mut instruction = instruction;
instruction.accounts.extend(resolved.accounts);
Ok(instruction)
}
Step::Fetch(addresses) => Err(HookError::NotLoaded(addresses).into()),
}
}
#[cfg(test)]
mod tests;